From d0510f52eb7efeabe00df5030cf10be16f99e178 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Thu, 15 Aug 2024 16:15:11 -0300 Subject: [PATCH] refactor(core): serialize response once, closes #5641 (#10641) * refactor(core): serialize response once closes #5641 This change impacts both the custom protocol and the postMessage based IPC implementations. Basically it changes the whole IPC mechanism to work on raw JSON strings so we do not need to serialize a serde_json::Value after serializing to it from a user-provided type. i benchmarked this with a 150MB file response (returning Vec instead of tauri::ipc::Response since the latter does not serialize at all) and it went from 29s to 23s (custom protocol) and from 54s to 48s (post message) on macOS. * fix mobile & lint * clippy --- .changes/refactor-ipc-response.md | 6 + core/tauri-config-schema/schema.json | 2 +- core/tauri-runtime-wry/src/lib.rs | 12 +- core/tauri-utils/src/config.rs | 4 +- core/tauri/src/app.rs | 7 ++ core/tauri/src/ipc/channel.rs | 15 ++- core/tauri/src/ipc/command.rs | 19 ++- core/tauri/src/ipc/format_callback.rs | 147 ++++++++++++++++++++--- core/tauri/src/ipc/mod.rs | 104 +++++++++------- core/tauri/src/ipc/protocol.rs | 122 ++++++++++++------- core/tauri/src/manager/mod.rs | 1 + core/tauri/src/test/mock_runtime.rs | 4 +- core/tauri/src/test/mod.rs | 4 +- core/tauri/src/webview/mod.rs | 8 +- core/tauri/src/webview/webview_window.rs | 11 +- core/tauri/src/window/mod.rs | 11 +- tooling/cli/schema.json | 2 +- 17 files changed, 343 insertions(+), 136 deletions(-) create mode 100644 .changes/refactor-ipc-response.md diff --git a/.changes/refactor-ipc-response.md b/.changes/refactor-ipc-response.md new file mode 100644 index 000000000000..f884d330a6c2 --- /dev/null +++ b/.changes/refactor-ipc-response.md @@ -0,0 +1,6 @@ +--- +"tauri": patch:breaking +--- + +Added a dedicated type for IPC response body `InvokeResponseBody` for performance reasons. +This is only a breaking change if you are directly using types from `tauri::ipc`. diff --git a/core/tauri-config-schema/schema.json b/core/tauri-config-schema/schema.json index 72cf80c655b6..90c2831a36c0 100644 --- a/core/tauri-config-schema/schema.json +++ b/core/tauri-config-schema/schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", - "description": "The Tauri configuration object.\n It is read from a file where you can define your frontend assets,\n configure the bundler and define a tray icon.\n\n The configuration file is generated by the\n [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in\n your Tauri application source directory (src-tauri).\n\n Once generated, you may modify it at will to customize your Tauri application.\n\n ## File Formats\n\n By default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\n Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\n The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\n The TOML file name is `Tauri.toml`.\n\n ## Platform-Specific Configuration\n\n In addition to the default configuration file, Tauri can\n read a platform-specific configuration from `tauri.linux.conf.json`,\n `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\n which gets merged with the main configuration object.\n\n ## Configuration Structure\n\n The configuration is composed of the following objects:\n\n - [`app`](#appconfig): The Tauri configuration\n - [`build`](#buildconfig): The build configuration\n - [`bundle`](#bundleconfig): The bundle configurations\n - [`plugins`](#pluginconfig): The plugins configuration\n\n ```json title=\"Example tauri.config.json file\"\n {\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"../dist\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n }\n ```", + "description": "The Tauri configuration object.\n It is read from a file where you can define your frontend assets,\n configure the bundler and define a tray icon.\n\n The configuration file is generated by the\n [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in\n your Tauri application source directory (src-tauri).\n\n Once generated, you may modify it at will to customize your Tauri application.\n\n ## File Formats\n\n By default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\n Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\n The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\n The TOML file name is `Tauri.toml`.\n\n ## Platform-Specific Configuration\n\n In addition to the default configuration file, Tauri can\n read a platform-specific configuration from `tauri.linux.conf.json`,\n `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\n which gets merged with the main configuration object.\n\n ## Configuration Structure\n\n The configuration is composed of the following objects:\n\n - [`app`](#appconfig): The Tauri configuration\n - [`build`](#buildconfig): The build configuration\n - [`bundle`](#bundleconfig): The bundle configurations\n - [`plugins`](#pluginconfig): The plugins configuration\n\n Example tauri.config.json file:\n\n ```json\n {\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"../dist\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n }\n ```", "type": "object", "properties": { "$schema": { diff --git a/core/tauri-runtime-wry/src/lib.rs b/core/tauri-runtime-wry/src/lib.rs index 56cf7900eec5..51242e688399 100644 --- a/core/tauri-runtime-wry/src/lib.rs +++ b/core/tauri-runtime-wry/src/lib.rs @@ -46,8 +46,8 @@ use wry::WebViewBuilderExtWindows; use tao::{ dpi::{ LogicalPosition as TaoLogicalPosition, LogicalSize as TaoLogicalSize, - LogicalUnit as ToaLogicalUnit, PhysicalPosition as TaoPhysicalPosition, - PhysicalSize as TaoPhysicalSize, Position as TaoPosition, Size as TaoSize, + PhysicalPosition as TaoPhysicalPosition, PhysicalSize as TaoPhysicalSize, + Position as TaoPosition, Size as TaoSize, }, event::{Event, StartCause, WindowEvent as TaoWindowEvent}, event_loop::{ @@ -793,16 +793,16 @@ impl WindowBuilder for WindowBuilderWrapper { let mut constraints = WindowSizeConstraints::default(); if let Some(min_width) = config.min_width { - constraints.min_width = Some(ToaLogicalUnit::new(min_width).into()); + constraints.min_width = Some(tao::dpi::LogicalUnit::new(min_width).into()); } if let Some(min_height) = config.min_height { - constraints.min_height = Some(ToaLogicalUnit::new(min_height).into()); + constraints.min_height = Some(tao::dpi::LogicalUnit::new(min_height).into()); } if let Some(max_width) = config.max_width { - constraints.max_width = Some(ToaLogicalUnit::new(max_width).into()); + constraints.max_width = Some(tao::dpi::LogicalUnit::new(max_width).into()); } if let Some(max_height) = config.max_height { - constraints.max_height = Some(ToaLogicalUnit::new(max_height).into()); + constraints.max_height = Some(tao::dpi::LogicalUnit::new(max_height).into()); } window = window.inner_size_constraints(constraints); diff --git a/core/tauri-utils/src/config.rs b/core/tauri-utils/src/config.rs index 4ca1c6139715..2089da339432 100644 --- a/core/tauri-utils/src/config.rs +++ b/core/tauri-utils/src/config.rs @@ -2170,7 +2170,9 @@ where /// - [`bundle`](#bundleconfig): The bundle configurations /// - [`plugins`](#pluginconfig): The plugins configuration /// -/// ```json title="Example tauri.config.json file" +/// Example tauri.config.json file: +/// +/// ```json /// { /// "productName": "tauri-app", /// "version": "0.1.0", diff --git a/core/tauri/src/app.rs b/core/tauri/src/app.rs index 1cda98bda808..b998d3044247 100644 --- a/core/tauri/src/app.rs +++ b/core/tauri/src/app.rs @@ -1741,6 +1741,13 @@ tauri::Builder::default() self.invoke_key, )); + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] let app_id = if manager.config.app.enable_gtk_app_id { Some(manager.config.identifier.clone()) } else { diff --git a/core/tauri/src/ipc/channel.rs b/core/tauri/src/ipc/channel.rs index 922186fc4e27..8d01dd16a8d5 100644 --- a/core/tauri/src/ipc/channel.rs +++ b/core/tauri/src/ipc/channel.rs @@ -20,7 +20,7 @@ use crate::{ Manager, Runtime, State, Webview, }; -use super::{CallbackFn, InvokeBody, InvokeError, IpcResponse, Request, Response}; +use super::{CallbackFn, InvokeError, InvokeResponseBody, IpcResponse, Request, Response}; pub const IPC_PAYLOAD_PREFIX: &str = "__CHANNEL__:"; pub const CHANNEL_PLUGIN_NAME: &str = "__TAURI_CHANNEL__"; @@ -33,13 +33,13 @@ static CHANNEL_DATA_COUNTER: AtomicU32 = AtomicU32::new(0); /// 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>>); +pub struct ChannelDataIpcQueue(pub(crate) Arc>>); /// An IPC channel. #[derive(Clone)] -pub struct Channel { +pub struct Channel { id: u32, - on_message: Arc crate::Result<()> + Send + Sync>, + on_message: Arc crate::Result<()> + Send + Sync>, phantom: std::marker::PhantomData, } @@ -138,13 +138,13 @@ impl<'de> Deserialize<'de> for JavaScriptChannelId { impl Channel { /// Creates a new channel with the given message handler. - pub fn new crate::Result<()> + Send + Sync + 'static>( + pub fn new crate::Result<()> + Send + Sync + 'static>( on_message: F, ) -> Self { Self::new_with_id(CHANNEL_COUNTER.fetch_add(1, Ordering::Relaxed), on_message) } - fn new_with_id crate::Result<()> + Send + Sync + 'static>( + fn new_with_id crate::Result<()> + Send + Sync + 'static>( id: u32, on_message: F, ) -> Self { @@ -195,8 +195,7 @@ impl Channel { where TSend: IpcResponse, { - let body = data.body()?; - (self.on_message)(body) + (self.on_message)(data.body()?) } } diff --git a/core/tauri/src/ipc/command.rs b/core/tauri/src/ipc/command.rs index dc99065cd275..af3596a89bc3 100644 --- a/core/tauri/src/ipc/command.rs +++ b/core/tauri/src/ipc/command.rs @@ -183,7 +183,7 @@ impl<'de, R: Runtime> Deserializer<'de> for CommandItem<'de, R> { #[doc(hidden)] pub mod private { use crate::{ - ipc::{InvokeBody, InvokeError, InvokeResolver, IpcResponse}, + ipc::{InvokeError, InvokeResolver, InvokeResponseBody, IpcResponse}, Runtime, }; use futures_util::{FutureExt, TryFutureExt}; @@ -220,7 +220,10 @@ pub mod private { } #[inline(always)] - pub fn future(self, value: T) -> impl Future> + pub fn future( + self, + value: T, + ) -> impl Future> where T: IpcResponse, { @@ -261,7 +264,7 @@ pub mod private { pub fn future( self, value: Result, - ) -> impl Future> + ) -> impl Future> where T: IpcResponse, E: Into, @@ -288,7 +291,10 @@ pub mod private { impl FutureTag { #[inline(always)] - pub fn future(self, value: F) -> impl Future> + pub fn future( + self, + value: F, + ) -> impl Future> where T: IpcResponse, F: Future + Send + 'static, @@ -315,7 +321,10 @@ pub mod private { impl ResultFutureTag { #[inline(always)] - pub fn future(self, value: F) -> impl Future> + pub fn future( + self, + value: F, + ) -> impl Future> where T: IpcResponse, E: Into, diff --git a/core/tauri/src/ipc/format_callback.rs b/core/tauri/src/ipc/format_callback.rs index 4f603846525b..0b75a82ac697 100644 --- a/core/tauri/src/ipc/format_callback.rs +++ b/core/tauri/src/ipc/format_callback.rs @@ -40,14 +40,14 @@ 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. -fn serialize_js_with String>( - value: &T, +fn serialize_js_with String>( + json_string: String, options: serialize_to_javascript::Options, cb: F, ) -> crate::Result { // get a raw &str representation of a serialized json value. - let string = serde_json::to_string(value)?; - let raw = RawValue::from_string(string)?; + + let raw = RawValue::from_string(json_string)?; // from here we know json.len() > 1 because an empty string is not a valid json value. let json = raw.get(); @@ -77,14 +77,21 @@ fn serialize_js_with String>( Ok(return_val) } -/// Formats a function name and argument to be evaluated as callback. +/// Formats a function name and a serializable argument to be evaluated as callback. +/// +/// See [`format_raw`] for more information. +pub fn format(function_name: CallbackFn, arg: &T) -> crate::Result { + format_raw(function_name, serde_json::to_string(arg)?) +} + +/// Formats a function name and a raw JSON string 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). -pub fn format(function_name: CallbackFn, arg: &T) -> crate::Result { - serialize_js_with(arg, Default::default(), |arg| { +pub fn format_raw(function_name: CallbackFn, json_string: String) -> crate::Result { + serialize_js_with(json_string, Default::default(), |arg| { format!( r#" if (window["_{fn}"]) {{ @@ -97,7 +104,21 @@ pub fn format(function_name: CallbackFn, arg: &T) -> crate::Result }) } -/// Formats a Result type to its Promise response. +/// Formats a serializable Result type to its Promise response. +/// +/// See [`format_result_raw`] for more information. +pub fn format_result( + result: Result, + success_callback: CallbackFn, + error_callback: CallbackFn, +) -> crate::Result { + match result { + Ok(res) => format(success_callback, &res), + Err(err) => format(error_callback, &err), + } +} + +/// Formats a Result type of raw JSON strings to its Promise response. /// Useful for Promises handling. /// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value. /// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value. @@ -107,14 +128,14 @@ pub fn format(function_name: CallbackFn, arg: &T) -> crate::Result /// * `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. -pub fn format_result( - result: Result, +pub fn format_result_raw( + raw_result: Result, success_callback: CallbackFn, error_callback: CallbackFn, ) -> crate::Result { - match result { - Ok(res) => format(success_callback, &res), - Err(err) => format(error_callback, &err), + match raw_result { + Ok(res) => format_raw(success_callback, res), + Err(err) => format_raw(error_callback, err), } } @@ -130,8 +151,31 @@ mod test { } } + #[derive(Debug, Clone)] + struct JsonStr(String); + + impl Arbitrary for JsonStr { + fn arbitrary(g: &mut Gen) -> Self { + if bool::arbitrary(g) { + Self(format!( + "{{ {}: {} }}", + serde_json::to_string(&String::arbitrary(g)).unwrap(), + serde_json::to_string(&String::arbitrary(g)).unwrap() + )) + } else { + Self(serde_json::to_string(&String::arbitrary(g)).unwrap()) + } + } + } + fn serialize_js(value: &T) -> crate::Result { - serialize_js_with(value, Default::default(), |v| v.into()) + serialize_js_with(serde_json::to_string(value)?, Default::default(), |v| { + v.into() + }) + } + + fn serialize_js_raw(value: impl Into) -> crate::Result { + serialize_js_with(value.into(), Default::default(), |v| v.into()) } #[test] @@ -213,4 +257,79 @@ mod test { serde_json::Value::String(value), )) } + + #[test] + fn test_serialize_js_raw() { + assert_eq!(serialize_js_raw("null").unwrap(), "null"); + assert_eq!(serialize_js_raw("5").unwrap(), "5"); + assert_eq!( + serialize_js_raw("{ \"x\": [1, 2, 3] }").unwrap(), + "{ \"x\": [1, 2, 3] }" + ); + + #[derive(serde::Serialize)] + struct JsonObj { + value: String, + } + + let raw_str = "T".repeat(MIN_JSON_PARSE_LEN); + assert_eq!( + serialize_js_raw(format!("\"{raw_str}\"")).unwrap(), + format!("\"{raw_str}\"") + ); + + assert_eq!( + serialize_js_raw(format!("{{\"value\":\"{raw_str}\"}}")).unwrap(), + format!("JSON.parse('{{\"value\":\"{raw_str}\"}}')") + ); + + assert_eq!( + serialize_js(&JsonObj { + value: format!("\"{raw_str}\"") + }) + .unwrap(), + format!("JSON.parse('{{\"value\":\"\\\\\"{raw_str}\\\\\"\"}}')") + ); + + let dangerous_json = RawValue::from_string( + r#"{"test":"don\\πŸš€πŸ±β€πŸ‘€\\'t forget to escape me!πŸš€πŸ±β€πŸ‘€","teπŸš€πŸ±β€πŸ‘€st2":"don't forget to escape me!","test3":"\\πŸš€πŸ±β€πŸ‘€\\\\'''\\\\πŸš€πŸ±β€πŸ‘€\\\\πŸš€πŸ±β€πŸ‘€\\'''''"}"#.into() + ).unwrap(); + + let definitely_escaped_dangerous_json = format!( + "JSON.parse('{}')", + dangerous_json + .get() + .replace('\\', "\\\\") + .replace('\'', "\\'") + ); + let escape_single_quoted_json_test = + serialize_to_javascript::Serialized::new(&dangerous_json, &Default::default()).into_string(); + + let result = r#"JSON.parse('{"test":"don\\\\πŸš€πŸ±β€πŸ‘€\\\\\'t forget to escape me!πŸš€πŸ±β€πŸ‘€","teπŸš€πŸ±β€πŸ‘€st2":"don\'t forget to escape me!","test3":"\\\\πŸš€πŸ±β€πŸ‘€\\\\\\\\\'\'\'\\\\\\\\πŸš€πŸ±β€πŸ‘€\\\\\\\\πŸš€πŸ±β€πŸ‘€\\\\\'\'\'\'\'"}')"#; + assert_eq!(definitely_escaped_dangerous_json, result); + assert_eq!(escape_single_quoted_json_test, result); + } + + // check arbitrary strings in the format callback function + #[quickcheck] + fn qc_formatting_raw(f: CallbackFn, a: JsonStr) -> bool { + let a = a.0; + // call format callback + let fc = format_raw(f, a.clone()).unwrap(); + fc.contains(&format!(r#"window["_{}"](JSON.parse('{}'))"#, f.0, a)) + || fc.contains(&format!(r#"window["_{}"]({})"#, f.0, a)) + } + + // check arbitrary strings in format_result + #[quickcheck] + fn qc_format_raw_res(result: Result, c: CallbackFn, ec: CallbackFn) -> bool { + let result = result.map(|v| v.0).map_err(|e| e.0); + let resp = format_result_raw(result.clone(), c, ec).expect("failed to format callback result"); + let (function, value) = match result { + Ok(v) => (c, v), + Err(e) => (ec, e), + }; + + resp.contains(&format!(r#"window["_{}"]({})"#, function.0, value)) + } } diff --git a/core/tauri/src/ipc/mod.rs b/core/tauri/src/ipc/mod.rs index ca23f182925b..05442f3cf9ac 100644 --- a/core/tauri/src/ipc/mod.rs +++ b/core/tauri/src/ipc/mod.rs @@ -72,14 +72,8 @@ impl From> for InvokeBody { } } -impl IpcResponse for InvokeBody { - fn body(self) -> crate::Result { - Ok(self) - } -} - impl InvokeBody { - #[allow(dead_code)] + #[cfg(mobile)] pub(crate) fn into_json(self) -> JsonValue { match self { Self::Json(v) => v, @@ -88,12 +82,51 @@ impl InvokeBody { } } } +} + +/// Possible values of an IPC response. +#[derive(Debug, Clone)] +#[cfg_attr(test, derive(PartialEq))] +pub enum InvokeResponseBody { + /// Json payload. + Json(String), + /// Bytes payload. + Raw(Vec), +} + +impl From for InvokeResponseBody { + fn from(value: String) -> Self { + Self::Json(value) + } +} + +impl From> for InvokeResponseBody { + fn from(value: Vec) -> Self { + Self::Raw(value) + } +} + +impl From for InvokeResponseBody { + fn from(value: InvokeBody) -> Self { + match value { + InvokeBody::Json(v) => Self::Json(serde_json::to_string(&v).unwrap()), + InvokeBody::Raw(v) => Self::Raw(v), + } + } +} + +impl IpcResponse for InvokeResponseBody { + fn body(self) -> crate::Result { + Ok(self) + } +} - /// Attempts to deserialize the invoke body. +impl InvokeResponseBody { + /// Attempts to deserialize the response. pub fn deserialize(self) -> serde_json::Result { match self { - InvokeBody::Json(v) => serde_json::from_value(v), - InvokeBody::Raw(v) => T::deserialize(v.into_deserializer()), + Self::Json(v) => serde_json::from_str(&v), + Self::Raw(v) => T::deserialize(v.into_deserializer()), } } } @@ -130,12 +163,12 @@ impl<'a, R: Runtime> CommandArg<'a, R> for Request<'a> { /// Marks a type as a response to an IPC call. pub trait IpcResponse { /// Resolve the IPC response body. - fn body(self) -> crate::Result; + fn body(self) -> crate::Result; } impl IpcResponse for T { - fn body(self) -> crate::Result { - serde_json::to_value(self) + fn body(self) -> crate::Result { + serde_json::to_string(&self) .map(Into::into) .map_err(Into::into) } @@ -143,18 +176,18 @@ impl IpcResponse for T { /// The IPC request. pub struct Response { - body: InvokeBody, + body: InvokeResponseBody, } impl IpcResponse for Response { - fn body(self) -> crate::Result { + fn body(self) -> crate::Result { Ok(self.body) } } impl Response { /// Defines a response with the given body. - pub fn new(body: impl Into) -> Self { + pub fn new(body: impl Into) -> Self { Self { body: body.into() } } } @@ -177,19 +210,19 @@ pub struct Invoke { /// Error response from an [`InvokeMessage`]. #[derive(Debug)] -pub struct InvokeError(pub JsonValue); +pub struct InvokeError(pub serde_json::Value); impl InvokeError { /// Create an [`InvokeError`] as a string of the [`std::error::Error`] message. #[inline(always)] pub fn from_error(error: E) -> Self { - Self(JsonValue::String(error.to_string())) + Self(serde_json::Value::String(error.to_string())) } /// Create an [`InvokeError`] as a string of the [`anyhow::Error`] message. #[inline(always)] pub fn from_anyhow(error: anyhow::Error) -> Self { - Self(JsonValue::String(format!("{error:#}"))) + Self(serde_json::Value::String(format!("{error:#}"))) } } @@ -205,7 +238,7 @@ impl From for InvokeError { impl From for InvokeError { #[inline(always)] fn from(error: crate::Error) -> Self { - Self(JsonValue::String(error.to_string())) + Self(serde_json::Value::String(error.to_string())) } } @@ -213,24 +246,11 @@ impl From for InvokeError { #[derive(Debug)] pub enum InvokeResponse { /// Resolve the promise. - Ok(InvokeBody), + Ok(InvokeResponseBody), /// Reject the promise. Err(InvokeError), } -impl Serialize for InvokeResponse { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - Self::Ok(InvokeBody::Json(j)) => j.serialize(serializer), - Self::Ok(InvokeBody::Raw(b)) => b.serialize(serializer), - Self::Err(e) => e.0.serialize(serializer), - } - } -} - impl> From> for InvokeResponse { #[inline] fn from(result: Result) -> Self { @@ -311,7 +331,7 @@ impl InvokeResolver { /// Reply to the invoke promise with an async task which is already serialized. pub fn respond_async_serialized(self, task: F) where - F: Future> + Send + 'static, + F: Future> + Send + 'static, { crate::async_runtime::spawn(async move { let response = match task.await { @@ -531,22 +551,18 @@ mod tests { use super::*; #[test] - fn deserialize_invoke_body() { - let json = InvokeBody::Json(serde_json::Value::Array(vec![ - serde_json::Value::Number(1.into()), - serde_json::Value::Number(123.into()), - serde_json::Value::Number(1231.into()), - ])); + fn deserialize_invoke_response_body() { + let json = InvokeResponseBody::Json("[1, 123, 1231]".to_string()); assert_eq!(json.deserialize::>().unwrap(), vec![1, 123, 1231]); - let json = InvokeBody::Json(serde_json::Value::String("string value".into())); + let json = InvokeResponseBody::Json("\"string value\"".to_string()); assert_eq!(json.deserialize::().unwrap(), "string value"); - let json = InvokeBody::Json(serde_json::Value::String("string value".into())); + let json = InvokeResponseBody::Json("\"string value\"".to_string()); assert!(json.deserialize::>().is_err()); let values = vec![1, 2, 3, 4, 5, 6, 1]; - let raw = InvokeBody::Raw(values.clone()); + let raw = InvokeResponseBody::Raw(values.clone()); assert_eq!(raw.deserialize::>().unwrap(), values); } } diff --git a/core/tauri/src/ipc/protocol.rs b/core/tauri/src/ipc/protocol.rs index 7698c0fe0df6..ef25f6542551 100644 --- a/core/tauri/src/ipc/protocol.rs +++ b/core/tauri/src/ipc/protocol.rs @@ -5,6 +5,7 @@ use std::{borrow::Cow, sync::Arc}; use crate::{ + ipc::InvokeResponseBody, manager::AppManager, webview::{InvokeRequest, UriSchemeProtocolHandler}, Runtime, @@ -18,7 +19,7 @@ use http::{ }; use url::Url; -use super::{CallbackFn, InvokeBody, InvokeResponse}; +use super::{CallbackFn, InvokeResponse}; const TAURI_CALLBACK_HEADER_NAME: &str = "Tauri-Callback"; const TAURI_ERROR_HEADER_NAME: &str = "Tauri-Error"; @@ -67,8 +68,8 @@ pub fn get(manager: Arc>, label: String) -> UriSchemeP span.record( "request", match &request.body { - InvokeBody::Json(j) => serde_json::to_string(j).unwrap(), - InvokeBody::Raw(b) => serde_json::to_string(b).unwrap(), + super::InvokeBody::Json(j) => serde_json::to_string(j).unwrap(), + super::InvokeBody::Raw(b) => serde_json::to_string(b).unwrap(), }, ); #[cfg(feature = "tracing")] @@ -85,12 +86,26 @@ pub fn get(manager: Arc>, label: String) -> UriSchemeP .entered(); #[cfg(feature = "tracing")] - let response_span = tracing::trace_span!( - "ipc::request::response", - response = serde_json::to_string(&response).unwrap(), - mime_type = tracing::field::Empty - ) - .entered(); + let response_span = match &response { + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => tracing::trace_span!( + "ipc::request::response", + response = v, + mime_type = tracing::field::Empty + ) + .entered(), + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => tracing::trace_span!( + "ipc::request::response", + response = format!("{v:?}"), + mime_type = tracing::field::Empty + ) + .entered(), + InvokeResponse::Err(e) => tracing::trace_span!( + "ipc::request::response", + error = format!("{e:?}"), + mime_type = tracing::field::Empty + ) + .entered(), + }; let response_header = match &response { InvokeResponse::Ok(_) => TAURI_RESPONSE_HEADER_OK, @@ -98,11 +113,11 @@ pub fn get(manager: Arc>, label: String) -> UriSchemeP }; let (mut response, mime_type) = match response { - InvokeResponse::Ok(InvokeBody::Json(v)) => ( - http::Response::new(serde_json::to_vec(&v).unwrap().into()), + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => ( + http::Response::new(v.as_bytes().to_vec().into()), mime::APPLICATION_JSON, ), - InvokeResponse::Ok(InvokeBody::Raw(v)) => ( + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => ( http::Response::new(v.into()), mime::APPLICATION_OCTET_STREAM, ), @@ -302,14 +317,8 @@ fn handle_ipc_message(request: Request, manager: &AppManager webview.on_message( request, Box::new(move |webview, cmd, response, callback, error| { - use crate::ipc::{ - format_callback::{ - format as format_callback, format_result as format_callback_result, - }, - Channel, - }; + use crate::ipc::Channel; use crate::sealed::ManagerBase; - use serde_json::Value as JsonValue; #[cfg(feature = "tracing")] let _respond_span = tracing::trace_span!( @@ -327,7 +336,7 @@ fn handle_ipc_message(request: Request, manager: &AppManager ) { let eval_js = match js { Ok(js) => js, - Err(e) => format_callback(error, &e.to_string()) + Err(e) => crate::ipc::format_callback::format(error, &e.to_string()) .expect("unable to serialize response error string to json"), }; @@ -339,51 +348,80 @@ fn handle_ipc_message(request: Request, manager: &AppManager && !options.custom_protocol_ipc_blocked; #[cfg(feature = "tracing")] - let _response_span = tracing::trace_span!( - "ipc::request::response", - response = serde_json::to_string(&response).unwrap(), - mime_type = match &response { - InvokeResponse::Ok(InvokeBody::Json(_)) => mime::APPLICATION_JSON, - InvokeResponse::Ok(InvokeBody::Raw(_)) => mime::APPLICATION_OCTET_STREAM, - InvokeResponse::Err(_) => mime::APPLICATION_JSON, - } - .essence_str() - ) - .entered(); + let mime_type = match &response { + InvokeResponse::Ok(InvokeResponseBody::Json(_)) => mime::APPLICATION_JSON, + InvokeResponse::Ok(InvokeResponseBody::Raw(_)) => mime::APPLICATION_OCTET_STREAM, + InvokeResponse::Err(_) => mime::APPLICATION_JSON, + }; - match &response { - InvokeResponse::Ok(InvokeBody::Json(v)) => { + #[cfg(feature = "tracing")] + let _response_span = match &response { + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => tracing::trace_span!( + "ipc::request::response", + response = v, + mime_type = mime_type.essence_str() + ) + .entered(), + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => tracing::trace_span!( + "ipc::request::response", + response = format!("{v:?}"), + mime_type = mime_type.essence_str() + ) + .entered(), + InvokeResponse::Err(e) => tracing::trace_span!( + "ipc::request::response", + response = format!("{e:?}"), + mime_type = mime_type.essence_str() + ) + .entered(), + }; + + match response { + InvokeResponse::Ok(InvokeResponseBody::Json(v)) => { if !(cfg!(target_os = "macos") || cfg!(target_os = "ios")) - && matches!(v, JsonValue::Object(_) | JsonValue::Array(_)) + && (v.starts_with('{') || v.starts_with('[')) && can_use_channel_for_response { - let _ = Channel::from_callback_fn(webview, callback).send(v); + let _ = Channel::from_callback_fn(webview, callback) + .send(InvokeResponseBody::Json(v)); } else { responder_eval( &webview, - format_callback_result(Result::<_, ()>::Ok(v), callback, error), + crate::ipc::format_callback::format_result_raw( + Result::<_, String>::Ok(v), + callback, + error, + ), error, ) } } - InvokeResponse::Ok(InvokeBody::Raw(v)) => { + InvokeResponse::Ok(InvokeResponseBody::Raw(v)) => { if cfg!(target_os = "macos") || cfg!(target_os = "ios") || !can_use_channel_for_response { responder_eval( &webview, - format_callback_result(Result::<_, ()>::Ok(v), callback, error), + crate::ipc::format_callback::format_result( + Result::<_, ()>::Ok(v), + callback, + error, + ), error, ); } else { - let _ = - Channel::from_callback_fn(webview, callback).send(InvokeBody::Raw(v.clone())); + let _ = Channel::from_callback_fn(webview, callback) + .send(InvokeResponseBody::Raw(v.clone())); } } InvokeResponse::Err(e) => responder_eval( &webview, - format_callback_result(Result::<(), _>::Err(&e.0), callback, error), + crate::ipc::format_callback::format_result( + Result::<(), _>::Err(&e.0), + callback, + error, + ), error, ), } @@ -530,7 +568,7 @@ mod tests { use std::str::FromStr; use super::*; - use crate::{manager::AppManager, plugin::PluginStore, StateManager, Wry}; + use crate::{ipc::InvokeBody, manager::AppManager, plugin::PluginStore, StateManager, Wry}; use http::header::*; use serde_json::json; use tauri_macros::generate_context; diff --git a/core/tauri/src/manager/mod.rs b/core/tauri/src/manager/mod.rs index 8210706d94d4..b095086234bc 100644 --- a/core/tauri/src/manager/mod.rs +++ b/core/tauri/src/manager/mod.rs @@ -615,6 +615,7 @@ impl AppManager { } } + #[cfg(desktop)] pub(crate) fn on_webview_close(&self, label: &str) { self.webview.webviews_lock().remove(label); diff --git a/core/tauri/src/test/mock_runtime.rs b/core/tauri/src/test/mock_runtime.rs index 1123fccb0ad3..449451b7d409 100644 --- a/core/tauri/src/test/mock_runtime.rs +++ b/core/tauri/src/test/mock_runtime.rs @@ -228,7 +228,7 @@ impl RuntimeHandle for MockRuntimeHandle { )) }); #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))] - return unimplemented!(); + unimplemented!(); } fn primary_monitor(&self) -> Option { @@ -725,7 +725,7 @@ impl WindowDispatch for MockWindowDispatcher { )) }; #[cfg(not(any(target_os = "linux", target_os = "macos", windows)))] - return unimplemented!(); + unimplemented!(); } fn center(&self) -> Result<()> { diff --git a/core/tauri/src/test/mod.rs b/core/tauri/src/test/mod.rs index c051096522f5..76d089a29cc9 100644 --- a/core/tauri/src/test/mod.rs +++ b/core/tauri/src/test/mod.rs @@ -58,7 +58,7 @@ use serialize_to_javascript::DefaultTemplate; use std::{borrow::Cow, collections::HashMap, fmt::Debug}; use crate::{ - ipc::{InvokeBody, InvokeError, InvokeResponse, RuntimeAuthority}, + ipc::{InvokeError, InvokeResponse, InvokeResponseBody, RuntimeAuthority}, webview::InvokeRequest, App, Assets, Builder, Context, Pattern, Runtime, Webview, }; @@ -282,7 +282,7 @@ pub fn assert_ipc_response< pub fn get_ipc_response>>( webview: &W, request: InvokeRequest, -) -> Result { +) -> Result { let (tx, rx) = std::sync::mpsc::sync_channel(1); webview.as_ref().clone().on_message( request, diff --git a/core/tauri/src/webview/mod.rs b/core/tauri/src/webview/mod.rs index 3bf5fe39125b..0c1be3164f6a 100644 --- a/core/tauri/src/webview/mod.rs +++ b/core/tauri/src/webview/mod.rs @@ -20,7 +20,7 @@ use tauri_runtime::{ }; use tauri_runtime::{ webview::{DetachedWebview, PendingWebview, WebviewAttributes}, - Rect, WebviewDispatch, + WebviewDispatch, }; use tauri_utils::config::{WebviewUrl, WindowConfig}; pub use url::Url; @@ -605,7 +605,7 @@ tauri::Builder::default() let mut pending = self.into_pending_webview(&window, window.label())?; - pending.webview_attributes.bounds = Some(Rect { size, position }); + pending.webview_attributes.bounds = Some(tauri_runtime::Rect { size, position }); let webview = match &mut window.runtime() { RuntimeOrDispatch::Dispatch(dispatcher) => dispatcher.create_webview(pending), @@ -902,7 +902,7 @@ impl Webview { } /// Resizes this webview. - pub fn set_bounds(&self, bounds: Rect) -> crate::Result<()> { + pub fn set_bounds(&self, bounds: tauri_runtime::Rect) -> crate::Result<()> { self .webview .dispatcher @@ -958,7 +958,7 @@ impl Webview { } /// Returns the bounds of the webviews's client area. - pub fn bounds(&self) -> crate::Result { + pub fn bounds(&self) -> crate::Result { self.webview.dispatcher.bounds().map_err(Into::into) } diff --git a/core/tauri/src/webview/webview_window.rs b/core/tauri/src/webview/webview_window.rs index 6e4fd045bf70..ba0f8ce7a4c1 100644 --- a/core/tauri/src/webview/webview_window.rs +++ b/core/tauri/src/webview/webview_window.rs @@ -27,7 +27,6 @@ use crate::{ }, }; use serde::Serialize; -use tauri_runtime::window::WindowSizeConstraints; use tauri_utils::config::{WebviewUrl, WindowConfig}; use url::Url; @@ -396,7 +395,10 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { /// Window inner size constraints. #[must_use] - pub fn inner_size_constraints(mut self, constraints: WindowSizeConstraints) -> Self { + pub fn inner_size_constraints( + mut self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> Self { self.window_builder = self.window_builder.inner_size_constraints(constraints); self } @@ -1465,7 +1467,10 @@ impl WebviewWindow { } /// Sets this window's minimum inner width. - pub fn set_size_constraints(&self, constriants: WindowSizeConstraints) -> crate::Result<()> { + pub fn set_size_constraints( + &self, + constriants: tauri_runtime::window::WindowSizeConstraints, + ) -> crate::Result<()> { self.webview.window().set_size_constraints(constriants) } diff --git a/core/tauri/src/window/mod.rs b/core/tauri/src/window/mod.rs index 6531cbd45038..fb3c2e8e2a49 100644 --- a/core/tauri/src/window/mod.rs +++ b/core/tauri/src/window/mod.rs @@ -9,7 +9,6 @@ pub(crate) mod plugin; use tauri_runtime::{ dpi::{PhysicalPosition, PhysicalSize}, webview::PendingWebview, - window::WindowSizeConstraints, }; pub use tauri_utils::{config::Color, WindowEffect as Effect, WindowEffectState as EffectState}; @@ -461,7 +460,10 @@ impl<'a, R: Runtime, M: Manager> WindowBuilder<'a, R, M> { /// Window inner size constraints. #[must_use] - pub fn inner_size_constraints(mut self, constraints: WindowSizeConstraints) -> Self { + pub fn inner_size_constraints( + mut self, + constraints: tauri_runtime::window::WindowSizeConstraints, + ) -> Self { self.window_builder = self.window_builder.inner_size_constraints(constraints); self } @@ -1830,7 +1832,10 @@ tauri::Builder::default() } /// Sets this window's minimum inner width. - pub fn set_size_constraints(&self, constriants: WindowSizeConstraints) -> crate::Result<()> { + pub fn set_size_constraints( + &self, + constriants: tauri_runtime::window::WindowSizeConstraints, + ) -> crate::Result<()> { self .window .dispatcher diff --git a/tooling/cli/schema.json b/tooling/cli/schema.json index 72cf80c655b6..90c2831a36c0 100644 --- a/tooling/cli/schema.json +++ b/tooling/cli/schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Config", - "description": "The Tauri configuration object.\n It is read from a file where you can define your frontend assets,\n configure the bundler and define a tray icon.\n\n The configuration file is generated by the\n [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in\n your Tauri application source directory (src-tauri).\n\n Once generated, you may modify it at will to customize your Tauri application.\n\n ## File Formats\n\n By default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\n Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\n The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\n The TOML file name is `Tauri.toml`.\n\n ## Platform-Specific Configuration\n\n In addition to the default configuration file, Tauri can\n read a platform-specific configuration from `tauri.linux.conf.json`,\n `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\n which gets merged with the main configuration object.\n\n ## Configuration Structure\n\n The configuration is composed of the following objects:\n\n - [`app`](#appconfig): The Tauri configuration\n - [`build`](#buildconfig): The build configuration\n - [`bundle`](#bundleconfig): The bundle configurations\n - [`plugins`](#pluginconfig): The plugins configuration\n\n ```json title=\"Example tauri.config.json file\"\n {\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"../dist\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n }\n ```", + "description": "The Tauri configuration object.\n It is read from a file where you can define your frontend assets,\n configure the bundler and define a tray icon.\n\n The configuration file is generated by the\n [`tauri init`](https://tauri.app/v1/api/cli#init) command that lives in\n your Tauri application source directory (src-tauri).\n\n Once generated, you may modify it at will to customize your Tauri application.\n\n ## File Formats\n\n By default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\n Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\n The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\n The TOML file name is `Tauri.toml`.\n\n ## Platform-Specific Configuration\n\n In addition to the default configuration file, Tauri can\n read a platform-specific configuration from `tauri.linux.conf.json`,\n `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\n which gets merged with the main configuration object.\n\n ## Configuration Structure\n\n The configuration is composed of the following objects:\n\n - [`app`](#appconfig): The Tauri configuration\n - [`build`](#buildconfig): The build configuration\n - [`bundle`](#bundleconfig): The bundle configurations\n - [`plugins`](#pluginconfig): The plugins configuration\n\n Example tauri.config.json file:\n\n ```json\n {\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"../dist\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n }\n ```", "type": "object", "properties": { "$schema": {