From 058c0db72f43fbe1574d0db654560e693755cd7e Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Tue, 5 Nov 2024 14:08:08 +0200 Subject: [PATCH 1/3] feat(bundler): add option to configure RPM compression (#11584) --- .changes/rpm-compression-level.md | 10 ++ crates/tauri-bundler/src/bundle/linux/rpm.rs | 18 ++- crates/tauri-bundler/src/bundle/settings.rs | 7 +- .../src/bundle/windows/nsis/mod.rs | 4 +- crates/tauri-cli/config.schema.json | 123 ++++++++++++++++++ crates/tauri-cli/src/interface/rust.rs | 1 + .../schemas/config.schema.json | 123 ++++++++++++++++++ crates/tauri-utils/src/config.rs | 33 +++++ 8 files changed, 314 insertions(+), 5 deletions(-) create mode 100644 .changes/rpm-compression-level.md diff --git a/.changes/rpm-compression-level.md b/.changes/rpm-compression-level.md new file mode 100644 index 000000000000..5a2017fea94d --- /dev/null +++ b/.changes/rpm-compression-level.md @@ -0,0 +1,10 @@ +--- +"tauri-bundler": "minor:feat" +"tauri-cli": "minor:feat" +"@tauri-apps/cli": "minor:feat" +"tauri-utils": "minor:feat" +--- + +Add `bundle > linux > rpm > compression` config option to control RPM bundle compression type and level. + + diff --git a/crates/tauri-bundler/src/bundle/linux/rpm.rs b/crates/tauri-bundler/src/bundle/linux/rpm.rs index 06cbc953dfa1..f01f6a8718a4 100644 --- a/crates/tauri-bundler/src/bundle/linux/rpm.rs +++ b/crates/tauri-bundler/src/bundle/linux/rpm.rs @@ -12,6 +12,7 @@ use std::{ fs::{self, File}, path::{Path, PathBuf}, }; +use tauri_utils::config::RpmCompression; use super::freedesktop; @@ -54,11 +55,24 @@ pub fn bundle_project(settings: &Settings) -> crate::Result> { let license = settings.license().unwrap_or_default(); let name = heck::AsKebabCase(settings.product_name()).to_string(); + + let compression = settings + .rpm() + .compression + .map(|c| match c { + RpmCompression::Gzip { level } => rpm::CompressionWithLevel::Gzip(level), + RpmCompression::Zstd { level } => rpm::CompressionWithLevel::Zstd(level), + RpmCompression::Xz { level } => rpm::CompressionWithLevel::Xz(level), + RpmCompression::Bzip2 { level } => rpm::CompressionWithLevel::Bzip2(level), + _ => rpm::CompressionWithLevel::None, + }) + // This matches .deb compression. On a 240MB source binary the bundle will be 100KB larger than rpm's default while reducing build times by ~25%. + .unwrap_or(rpm::CompressionWithLevel::Gzip(6)); + let mut builder = rpm::PackageBuilder::new(&name, version, &license, arch, summary) .epoch(epoch) .release(release) - // This matches .deb compression. On a 240MB source binary the bundle will be 100KB larger than rpm's default while reducing build times by ~25%. - .compression(rpm::CompressionWithLevel::Gzip(6)); + .compression(compression); if let Some(description) = settings.long_description() { builder = builder.description(description); diff --git a/crates/tauri-bundler/src/bundle/settings.rs b/crates/tauri-bundler/src/bundle/settings.rs index 610c6e749f1e..a29398f76b98 100644 --- a/crates/tauri-bundler/src/bundle/settings.rs +++ b/crates/tauri-bundler/src/bundle/settings.rs @@ -8,7 +8,10 @@ use crate::bundle::{common, platform::target_triple}; use anyhow::Context; pub use tauri_utils::config::WebviewInstallMode; use tauri_utils::{ - config::{BundleType, DeepLinkProtocol, FileAssociation, NSISInstallerMode, NsisCompression}, + config::{ + BundleType, DeepLinkProtocol, FileAssociation, NSISInstallerMode, NsisCompression, + RpmCompression, + }, resources::{external_binaries, ResourcePaths}, }; @@ -262,6 +265,8 @@ pub struct RpmSettings { /// Path to script that will be executed after the package is removed. See /// pub post_remove_script: Option, + /// Compression algorithm and level. Defaults to `Gzip` with level 6. + pub compression: Option, } /// Position coordinates struct. diff --git a/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs b/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs index 3d281489c369..810141214c5c 100644 --- a/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs +++ b/crates/tauri-bundler/src/bundle/windows/nsis/mod.rs @@ -90,8 +90,8 @@ pub fn bundle_project(settings: &Settings, updater: bool) -> crate::Result", "type": "object", diff --git a/crates/tauri-cli/src/interface/rust.rs b/crates/tauri-cli/src/interface/rust.rs index 08008171427b..8a407f8eebe7 100644 --- a/crates/tauri-cli/src/interface/rust.rs +++ b/crates/tauri-cli/src/interface/rust.rs @@ -1388,6 +1388,7 @@ fn tauri_config_to_bundle_settings( post_install_script: config.linux.rpm.post_install_script, pre_remove_script: config.linux.rpm.pre_remove_script, post_remove_script: config.linux.rpm.post_remove_script, + compression: config.linux.rpm.compression, }, dmg: DmgSettings { background: config.macos.dmg.background, diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index 047c4a7393e0..90acf8973183 100644 --- a/crates/tauri-schema-generator/schemas/config.schema.json +++ b/crates/tauri-schema-generator/schemas/config.schema.json @@ -2789,10 +2789,133 @@ "string", "null" ] + }, + "compression": { + "description": "Compression algorithm and level. Defaults to `Gzip` with level 6.", + "anyOf": [ + { + "$ref": "#/definitions/RpmCompression" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false }, + "RpmCompression": { + "description": "Compression algorithms used when bundling RPM packages.", + "oneOf": [ + { + "description": "Gzip compression", + "type": "object", + "required": [ + "level", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "gzip" + ] + }, + "level": { + "description": "Gzip compression level", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "Zstd compression", + "type": "object", + "required": [ + "level", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "zstd" + ] + }, + "level": { + "description": "Zstd compression level", + "type": "integer", + "format": "int32" + } + }, + "additionalProperties": false + }, + { + "description": "Xz compression", + "type": "object", + "required": [ + "level", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "xz" + ] + }, + "level": { + "description": "Xz compression level", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "Bzip2 compression", + "type": "object", + "required": [ + "level", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "bzip2" + ] + }, + "level": { + "description": "Bzip2 compression level", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + { + "description": "Disable compression", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "none" + ] + } + }, + "additionalProperties": false + } + ] + }, "MacConfig": { "description": "Configuration for the macOS bundles.\n\n See more: ", "type": "object", diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index 3780079bf1e5..041deda585b1 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -392,6 +392,36 @@ pub struct LinuxConfig { pub rpm: RpmConfig, } +/// Compression algorithms used when bundling RPM packages. +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +#[cfg_attr(feature = "schema", derive(JsonSchema))] +#[serde(rename_all = "camelCase", deny_unknown_fields, tag = "type")] +#[non_exhaustive] +pub enum RpmCompression { + /// Gzip compression + Gzip { + /// Gzip compression level + level: u32, + }, + /// Zstd compression + Zstd { + /// Zstd compression level + level: i32, + }, + /// Xz compression + Xz { + /// Xz compression level + level: u32, + }, + /// Bzip2 compression + Bzip2 { + /// Bzip2 compression level + level: u32, + }, + /// Disable compression + None, +} + /// Configuration for RPM bundles. #[skip_serializing_none] #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] @@ -440,6 +470,8 @@ pub struct RpmConfig { /// #[serde(alias = "post-remove-script")] pub post_remove_script: Option, + /// Compression algorithm and level. Defaults to `Gzip` with level 6. + pub compression: Option, } impl Default for RpmConfig { @@ -458,6 +490,7 @@ impl Default for RpmConfig { post_install_script: None, pre_remove_script: None, post_remove_script: None, + compression: None, } } } From f37e97d410c4a219e99f97692da05ca9d8e0ba3a Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Tue, 5 Nov 2024 14:48:59 +0200 Subject: [PATCH 2/3] feat: add `use_https_scheme` for Windows and Android (#11477) * feat: add `use_https_scheme` for Windows and Android closes #11252 * fix compilation * Apply suggestions from code review Co-authored-by: Fabian-Lars * change wording * add migrations * migrate `dangerousUseHttpScheme` * fmt * infer AssetResolver::get https scheme config * fix tests --------- Co-authored-by: Fabian-Lars Co-authored-by: Lucas Nogueira --- .../use_https_windows-and-android-config.md | 6 ++ .changes/use_https_windows-and-android.md | 7 ++ crates/tauri-cli/config.schema.json | 5 ++ .../src/migrate/migrations/v1/config.rs | 49 +++++++++++- crates/tauri-runtime-wry/src/lib.rs | 79 +++++++++++++------ crates/tauri-runtime/src/webview.rs | 18 +++++ crates/tauri-runtime/src/window.rs | 18 ++++- .../schemas/config.schema.json | 5 ++ crates/tauri-utils/src/config.rs | 17 +++- crates/tauri/scripts/core.js | 3 +- crates/tauri/src/app.rs | 19 ++++- crates/tauri/src/manager/mod.rs | 29 +++++-- crates/tauri/src/manager/webview.rs | 23 ++++-- crates/tauri/src/pattern.rs | 5 +- crates/tauri/src/protocol/isolation.rs | 4 +- crates/tauri/src/protocol/tauri.rs | 8 +- crates/tauri/src/test/mock_runtime.rs | 57 +++++++------ crates/tauri/src/webview/mod.rs | 46 +++++++++-- crates/tauri/src/webview/webview_window.rs | 15 ++++ crates/tauri/src/window/mod.rs | 6 +- examples/api/src-tauri/src/lib.rs | 3 +- packages/api/src/webview.ts | 15 ++++ 22 files changed, 358 insertions(+), 79 deletions(-) create mode 100644 .changes/use_https_windows-and-android-config.md create mode 100644 .changes/use_https_windows-and-android.md diff --git a/.changes/use_https_windows-and-android-config.md b/.changes/use_https_windows-and-android-config.md new file mode 100644 index 000000000000..3e5598a89e42 --- /dev/null +++ b/.changes/use_https_windows-and-android-config.md @@ -0,0 +1,6 @@ +--- +"tauri": "minor:feat" +"tauri-utils": "minor:feat" +--- + +Add `app > windows > useHttpsScheme` config option to choose whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android diff --git a/.changes/use_https_windows-and-android.md b/.changes/use_https_windows-and-android.md new file mode 100644 index 000000000000..0e1bbb1a6c6d --- /dev/null +++ b/.changes/use_https_windows-and-android.md @@ -0,0 +1,7 @@ +--- +"tauri": "minor:feat" +"tauri-runtime": "minor:feat" +"tauri-runtime-wry": "minor:feat" +--- + +Add `WebviewWindowBuilder/WebviewBuilder::use_https_scheme` to choose whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android diff --git a/crates/tauri-cli/config.schema.json b/crates/tauri-cli/config.schema.json index 90acf8973183..820fb35a55f0 100644 --- a/crates/tauri-cli/config.schema.json +++ b/crates/tauri-cli/config.schema.json @@ -486,6 +486,11 @@ "description": "Whether browser extensions can be installed for the webview process\n\n ## Platform-specific:\n\n - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled)\n - **MacOS / Linux / iOS / Android** - Unsupported.", "default": false, "type": "boolean" + }, + "useHttpsScheme": { + "description": "Sets whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android. Defaults to `false`.\n\n ## Note\n\n Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `://localhost` protocols used on macOS and Linux.\n\n ## Warning\n\n Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data.", + "default": false, + "type": "boolean" } }, "additionalProperties": false diff --git a/crates/tauri-cli/src/migrate/migrations/v1/config.rs b/crates/tauri-cli/src/migrate/migrations/v1/config.rs index ab956e76aa19..2b211470a096 100644 --- a/crates/tauri-cli/src/migrate/migrations/v1/config.rs +++ b/crates/tauri-cli/src/migrate/migrations/v1/config.rs @@ -87,6 +87,28 @@ fn migrate_config(config: &mut Value) -> Result { migrated.permissions = permissions; } + // dangerousUseHttpScheme/useHttpsScheme + let dangerouse_use_http = tauri_config + .get("security") + .and_then(|w| w.as_object()) + .and_then(|w| { + w.get("dangerousUseHttpScheme") + .or_else(|| w.get("dangerous-use-http-scheme")) + }) + .and_then(|v| v.as_bool()) + .unwrap_or_default(); + + if let Some(windows) = tauri_config + .get_mut("windows") + .and_then(|w| w.as_array_mut()) + { + for window in windows { + if let Some(window) = window.as_object_mut() { + window.insert("useHttpsScheme".to_string(), (!dangerouse_use_http).into()); + } + } + } + // security if let Some(security) = tauri_config .get_mut("security") @@ -802,7 +824,8 @@ mod test { "pattern": { "use": "brownfield" }, "security": { "csp": "default-src 'self' tauri:" - } + }, + "windows": [{}] } }); @@ -907,6 +930,8 @@ mod test { migrated["app"]["withGlobalTauri"], original["build"]["withGlobalTauri"] ); + + assert_eq!(migrated["app"]["windows"][0]["useHttpsScheme"], true); } #[test] @@ -941,6 +966,28 @@ mod test { ); } + #[test] + fn migrate_dangerous_use_http_scheme() { + let original = serde_json::json!({ + "tauri": { + "windows": [{}], + "security": { + "dangerousUseHttpScheme": true, + } + } + }); + + let migrated = migrate(&original); + assert_eq!( + !migrated["app"]["windows"][0]["useHttpsScheme"] + .as_bool() + .unwrap(), + original["tauri"]["security"]["dangerousUseHttpScheme"] + .as_bool() + .unwrap() + ); + } + #[test] fn can_migrate_default_config() { let original = serde_json::to_value(tauri_utils_v1::config::Config::default()).unwrap(); diff --git a/crates/tauri-runtime-wry/src/lib.rs b/crates/tauri-runtime-wry/src/lib.rs index 0e2f23386f46..838def7790ec 100644 --- a/crates/tauri-runtime-wry/src/lib.rs +++ b/crates/tauri-runtime-wry/src/lib.rs @@ -22,8 +22,8 @@ use tauri_runtime::{ monitor::Monitor, webview::{DetachedWebview, DownloadEvent, PendingWebview, WebviewIpcHandler}, window::{ - CursorIcon, DetachedWindow, DragDropEvent, PendingWindow, RawWindow, WebviewEvent, - WindowBuilder, WindowBuilderBase, WindowEvent, WindowId, WindowSizeConstraints, + CursorIcon, DetachedWindow, DetachedWindowWebview, DragDropEvent, PendingWindow, RawWindow, + WebviewEvent, WindowBuilder, WindowBuilderBase, WindowEvent, WindowId, WindowSizeConstraints, }, DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Icon, ProgressBarState, ProgressBarStatus, Result, RunEvent, Runtime, RuntimeHandle, RuntimeInitArgs, UserAttentionType, @@ -276,7 +276,16 @@ impl Context { let label = pending.label.clone(); let context = self.clone(); let window_id = self.next_window_id(); - let webview_id = pending.webview.as_ref().map(|_| context.next_webview_id()); + let (webview_id, use_https_scheme) = pending + .webview + .as_ref() + .map(|w| { + ( + Some(context.next_webview_id()), + w.webview_attributes.use_https_scheme, + ) + }) + .unwrap_or((None, false)); send_user_message( self, @@ -300,13 +309,19 @@ impl Context { context: self.clone(), }; - let detached_webview = webview_id.map(|id| DetachedWebview { - label: label.clone(), - dispatcher: WryWebviewDispatcher { - window_id: Arc::new(Mutex::new(window_id)), - webview_id: id, - context: self.clone(), - }, + let detached_webview = webview_id.map(|id| { + let webview = DetachedWebview { + label: label.clone(), + dispatcher: WryWebviewDispatcher { + window_id: Arc::new(Mutex::new(window_id)), + webview_id: id, + context: self.clone(), + }, + }; + DetachedWindowWebview { + webview, + use_https_scheme, + } }); Ok(DetachedWindow { @@ -746,6 +761,8 @@ impl WindowBuilder for WindowBuilderWrapper { builder = builder.title_bar_style(TitleBarStyle::Visible); } + builder = builder.title("Tauri App"); + builder } @@ -2497,10 +2514,16 @@ impl Runtime for Wry { ) -> Result> { let label = pending.label.clone(); let window_id = self.context.next_window_id(); - let webview_id = pending + let (webview_id, use_https_scheme) = pending .webview .as_ref() - .map(|_| self.context.next_webview_id()); + .map(|w| { + ( + Some(self.context.next_webview_id()), + w.webview_attributes.use_https_scheme, + ) + }) + .unwrap_or((None, false)); let window = create_window( window_id, @@ -2524,13 +2547,19 @@ impl Runtime for Wry { .borrow_mut() .insert(window_id, window); - let detached_webview = webview_id.map(|id| DetachedWebview { - label: label.clone(), - dispatcher: WryWebviewDispatcher { - window_id: Arc::new(Mutex::new(window_id)), - webview_id: id, - context: self.context.clone(), - }, + let detached_webview = webview_id.map(|id| { + let webview = DetachedWebview { + label: label.clone(), + dispatcher: WryWebviewDispatcher { + window_id: Arc::new(Mutex::new(window_id)), + webview_id: id, + context: self.context.clone(), + }, + }; + DetachedWindowWebview { + webview, + use_https_scheme, + } }); Ok(DetachedWindow { @@ -4026,6 +4055,11 @@ fn create_webview( .with_clipboard(webview_attributes.clipboard) .with_hotkeys_zoom(webview_attributes.zoom_hotkeys_enabled); + #[cfg(any(target_os = "windows", target_os = "android"))] + { + webview_builder = webview_builder.with_https_scheme(webview_attributes.use_https_scheme); + } + if webview_attributes.drag_drop_handler_enabled { let proxy = context.proxy.clone(); let window_id_ = window_id.clone(); @@ -4168,11 +4202,6 @@ fn create_webview( }); } - #[cfg(windows)] - { - webview_builder = webview_builder.with_https_scheme(false); - } - #[cfg(windows)] { webview_builder = webview_builder @@ -4282,7 +4311,7 @@ fn create_webview( builder } } - .map_err(|e| Error::CreateWebview(Box::new(dbg!(e))))?; + .map_err(|e| Error::CreateWebview(Box::new(e)))?; if kind == WebviewKind::WindowContent { #[cfg(any( diff --git a/crates/tauri-runtime/src/webview.rs b/crates/tauri-runtime/src/webview.rs index 651aaec5eac3..149983808895 100644 --- a/crates/tauri-runtime/src/webview.rs +++ b/crates/tauri-runtime/src/webview.rs @@ -210,6 +210,7 @@ pub struct WebviewAttributes { pub proxy_url: Option, pub zoom_hotkeys_enabled: bool, pub browser_extensions_enabled: bool, + pub use_https_scheme: bool, } impl From<&WindowConfig> for WebviewAttributes { @@ -218,6 +219,7 @@ impl From<&WindowConfig> for WebviewAttributes { .incognito(config.incognito) .focused(config.focus) .zoom_hotkeys_enabled(config.zoom_hotkeys_enabled) + .use_https_scheme(config.use_https_scheme) .browser_extensions_enabled(config.browser_extensions_enabled); #[cfg(any(not(target_os = "macos"), feature = "macos-private-api"))] { @@ -264,6 +266,7 @@ impl WebviewAttributes { proxy_url: None, zoom_hotkeys_enabled: false, browser_extensions_enabled: false, + use_https_scheme: false, } } @@ -388,6 +391,21 @@ impl WebviewAttributes { self.browser_extensions_enabled = enabled; self } + + /// Sets whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android. Defaults to `false`. + /// + /// ## Note + /// + /// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `://localhost` protocols used on macOS and Linux. + /// + /// ## Warning + /// + /// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data. + #[must_use] + pub fn use_https_scheme(mut self, enabled: bool) -> Self { + self.use_https_scheme = enabled; + self + } } /// IPC handler. diff --git a/crates/tauri-runtime/src/window.rs b/crates/tauri-runtime/src/window.rs index 54676ef65bf9..cea526f5f120 100644 --- a/crates/tauri-runtime/src/window.rs +++ b/crates/tauri-runtime/src/window.rs @@ -512,7 +512,23 @@ pub struct DetachedWindow> { pub dispatcher: R::WindowDispatcher, /// The webview dispatcher in case this window has an attached webview. - pub webview: Option>, + pub webview: Option>, +} + +/// A detached webview associated with a window. +#[derive(Debug)] +pub struct DetachedWindowWebview> { + pub webview: DetachedWebview, + pub use_https_scheme: bool, +} + +impl> Clone for DetachedWindowWebview { + fn clone(&self) -> Self { + Self { + webview: self.webview.clone(), + use_https_scheme: self.use_https_scheme, + } + } } impl> Clone for DetachedWindow { diff --git a/crates/tauri-schema-generator/schemas/config.schema.json b/crates/tauri-schema-generator/schemas/config.schema.json index 90acf8973183..820fb35a55f0 100644 --- a/crates/tauri-schema-generator/schemas/config.schema.json +++ b/crates/tauri-schema-generator/schemas/config.schema.json @@ -486,6 +486,11 @@ "description": "Whether browser extensions can be installed for the webview process\n\n ## Platform-specific:\n\n - **Windows**: Enables the WebView2 environment's [`AreBrowserExtensionsEnabled`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/winrt/microsoft_web_webview2_core/corewebview2environmentoptions?view=webview2-winrt-1.0.2739.15#arebrowserextensionsenabled)\n - **MacOS / Linux / iOS / Android** - Unsupported.", "default": false, "type": "boolean" + }, + "useHttpsScheme": { + "description": "Sets whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android. Defaults to `false`.\n\n ## Note\n\n Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `://localhost` protocols used on macOS and Linux.\n\n ## Warning\n\n Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data.", + "default": false, + "type": "boolean" } }, "additionalProperties": false diff --git a/crates/tauri-utils/src/config.rs b/crates/tauri-utils/src/config.rs index 041deda585b1..4affc4c87cd2 100644 --- a/crates/tauri-utils/src/config.rs +++ b/crates/tauri-utils/src/config.rs @@ -1519,6 +1519,18 @@ pub struct WindowConfig { /// - **MacOS / Linux / iOS / Android** - Unsupported. #[serde(default)] pub browser_extensions_enabled: bool, + + /// Sets whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android. Defaults to `false`. + /// + /// ## Note + /// + /// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `://localhost` protocols used on macOS and Linux. + /// + /// ## Warning + /// + /// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data. + #[serde(default, alias = "use-https-scheme")] + pub use_https_scheme: bool, } impl Default for WindowConfig { @@ -1567,6 +1579,7 @@ impl Default for WindowConfig { proxy_url: None, zoom_hotkeys_enabled: false, browser_extensions_enabled: false, + use_https_scheme: false, } } } @@ -2538,6 +2551,7 @@ mod build { let parent = opt_str_lit(self.parent.as_ref()); let zoom_hotkeys_enabled = self.zoom_hotkeys_enabled; let browser_extensions_enabled = self.browser_extensions_enabled; + let use_https_scheme = self.use_https_scheme; literal_struct!( tokens, @@ -2584,7 +2598,8 @@ mod build { incognito, parent, zoom_hotkeys_enabled, - browser_extensions_enabled + browser_extensions_enabled, + use_https_scheme ); } } diff --git a/crates/tauri/scripts/core.js b/crates/tauri/scripts/core.js index 6f4523c8d67a..b68b6039195b 100644 --- a/crates/tauri/scripts/core.js +++ b/crates/tauri/scripts/core.js @@ -8,12 +8,13 @@ } const osName = __TEMPLATE_os_name__ + const protocolScheme = __TEMPLATE_protocol_scheme__ Object.defineProperty(window.__TAURI_INTERNALS__, 'convertFileSrc', { value: function (filePath, protocol = 'asset') { const path = encodeURIComponent(filePath) return osName === 'windows' || osName === 'android' - ? `http://${protocol}.localhost/${path}` + ? `${protocolScheme}://${protocol}.localhost/${path}` : `${protocol}://localhost/${path}` } }) diff --git a/crates/tauri/src/app.rs b/crates/tauri/src/app.rs index b513ed7f8964..509ba340c8f4 100644 --- a/crates/tauri/src/app.rs +++ b/crates/tauri/src/app.rs @@ -268,6 +268,10 @@ pub struct AssetResolver { impl AssetResolver { /// Gets the app asset associated with the given path. /// + /// By default it tries to infer your application's URL scheme in production by checking if all webviews + /// were configured with [`crate::webview::WebviewBuilder::use_https_scheme`] or `tauri.conf.json > app > windows > useHttpsScheme`. + /// If you are resolving an asset for a webview with a more dynamic configuration, see [`AssetResolver::get_for_scheme`]. + /// /// Resolves to the embedded asset that is part of the app /// in dev when [`devUrl`](https://v2.tauri.app/reference/config/#devurl) points to a folder in your filesystem /// or in production when [`frontendDist`](https://v2.tauri.app/reference/config/#frontenddist) @@ -276,6 +280,19 @@ impl AssetResolver { /// Fallbacks to reading the asset from the [distDir] folder so the behavior is consistent in development. /// Note that the dist directory must exist so you might need to build your frontend assets first. pub fn get(&self, path: String) -> Option { + let use_https_scheme = self + .manager + .webviews() + .values() + .all(|webview| webview.use_https_scheme()); + self.get_for_scheme(path, use_https_scheme) + } + + /// Same as [AssetResolver::get] but resolves the custom protocol scheme based on a parameter. + /// + /// - `use_https_scheme`: If `true` when using [`Pattern::Isolation`](tauri::Pattern::Isolation), + /// the csp header will contain `https://tauri.localhost` instead of `http://tauri.localhost` + pub fn get_for_scheme(&self, path: String, use_https_scheme: bool) -> Option { #[cfg(dev)] { // on dev if the devPath is a path to a directory we have the embedded assets @@ -306,7 +323,7 @@ impl AssetResolver { } } - self.manager.get_asset(path).ok() + self.manager.get_asset(path, use_https_scheme).ok() } /// Iterate on all assets. diff --git a/crates/tauri/src/manager/mod.rs b/crates/tauri/src/manager/mod.rs index d272b241bfb9..81f0eaae284d 100644 --- a/crates/tauri/src/manager/mod.rs +++ b/crates/tauri/src/manager/mod.rs @@ -340,9 +340,10 @@ impl AppManager { self.config.build.dev_url.as_ref() } - pub(crate) fn protocol_url(&self) -> Cow<'_, Url> { + pub(crate) fn protocol_url(&self, https: bool) -> Cow<'_, Url> { if cfg!(windows) || cfg!(target_os = "android") { - Cow::Owned(Url::parse("http://tauri.localhost").unwrap()) + let scheme = if https { "https" } else { "http" }; + Cow::Owned(Url::parse(&format!("{scheme}://tauri.localhost")).unwrap()) } else { Cow::Owned(Url::parse("tauri://localhost").unwrap()) } @@ -351,10 +352,10 @@ impl AppManager { /// Get the base URL to use for webview requests. /// /// In dev mode, this will be based on the `devUrl` configuration value. - pub(crate) fn get_url(&self) -> Cow<'_, Url> { + pub(crate) fn get_url(&self, https: bool) -> Cow<'_, Url> { match self.base_path() { Some(url) => Cow::Borrowed(url), - _ => self.protocol_url(), + _ => self.protocol_url(https), } } @@ -372,7 +373,11 @@ impl AppManager { } } - pub fn get_asset(&self, mut path: String) -> Result> { + pub fn get_asset( + &self, + mut path: String, + use_https_schema: bool, + ) -> Result> { let assets = &self.assets; if path.ends_with('/') { path.pop(); @@ -435,7 +440,7 @@ impl AppManager { let default_src = csp_map .entry("default-src".into()) .or_insert_with(Default::default); - default_src.push(crate::pattern::format_real_schema(schema)); + default_src.push(crate::pattern::format_real_schema(schema, use_https_schema)); } csp_header.replace(Csp::DirectiveMap(csp_map).to_string()); @@ -771,17 +776,25 @@ mod test { #[cfg(custom_protocol)] { assert_eq!( - manager.get_url().to_string(), + manager.get_url(false).to_string(), if cfg!(windows) || cfg!(target_os = "android") { "http://tauri.localhost/" } else { "tauri://localhost" } ); + assert_eq!( + manager.get_url(true).to_string(), + if cfg!(windows) || cfg!(target_os = "android") { + "https://tauri.localhost/" + } else { + "tauri://localhost" + } + ); } #[cfg(dev)] - assert_eq!(manager.get_url().to_string(), "http://localhost:4000/"); + assert_eq!(manager.get_url(false).to_string(), "http://localhost:4000/"); } struct EventSetup { diff --git a/crates/tauri/src/manager/webview.rs b/crates/tauri/src/manager/webview.rs index 5eb4ba8de2a3..8773600dca2e 100644 --- a/crates/tauri/src/manager/webview.rs +++ b/crates/tauri/src/manager/webview.rs @@ -137,10 +137,14 @@ impl WebviewManager { let mut webview_attributes = pending.webview_attributes; + let use_https_scheme = webview_attributes.use_https_scheme; + let ipc_init = IpcJavascript { isolation_origin: &match &*app_manager.pattern { #[cfg(feature = "isolation")] - crate::Pattern::Isolation { schema, .. } => crate::pattern::format_real_schema(schema), + crate::Pattern::Isolation { schema, .. } => { + crate::pattern::format_real_schema(schema, use_https_scheme) + } _ => "".to_string(), }, } @@ -180,6 +184,7 @@ impl WebviewManager { &ipc_init.into_string(), &pattern_init.into_string(), is_init_global, + use_https_scheme, )?); for plugin_init_script in plugin_init_scripts { @@ -190,7 +195,7 @@ impl WebviewManager { if let crate::Pattern::Isolation { schema, .. } = &*app_manager.pattern { webview_attributes = webview_attributes.initialization_script( &IsolationJavascript { - isolation_src: &crate::pattern::format_real_schema(schema), + isolation_src: &crate::pattern::format_real_schema(schema, use_https_scheme), style: tauri_utils::pattern::isolation::IFRAME_STYLE, } .render_default(&Default::default())? @@ -232,7 +237,8 @@ impl WebviewManager { && window_url.scheme() != "http" && window_url.scheme() != "https" { - format!("http://{}.localhost", window_url.scheme()) + let https = if use_https_scheme { "https" } else { "http" }; + format!("{https}://{}.localhost", window_url.scheme()) } else if let Some(host) = window_url.host() { format!( "{}://{}{}", @@ -320,6 +326,7 @@ impl WebviewManager { assets.clone(), *crypto_keys.aes_gcm().raw(), window_origin, + use_https_scheme, ); pending.register_uri_scheme_protocol(schema, move |webview_id, request, responder| { protocol(webview_id, request, UriSchemeResponder(responder)) @@ -335,6 +342,7 @@ impl WebviewManager { ipc_script: &str, pattern_script: &str, with_global_tauri: bool, + use_https_scheme: bool, ) -> crate::Result { #[derive(Template)] #[default_template("../../scripts/init.js")] @@ -357,6 +365,7 @@ impl WebviewManager { #[default_template("../../scripts/core.js")] struct CoreJavascript<'a> { os_name: &'a str, + protocol_scheme: &'a str, invoke_key: &'a str, } @@ -378,6 +387,7 @@ impl WebviewManager { bundle_script, core_script: &CoreJavascript { os_name: std::env::consts::OS, + protocol_scheme: if use_https_scheme { "https" } else { "http" }, invoke_key: self.invoke_key(), } .render_default(&Default::default())? @@ -411,7 +421,7 @@ impl WebviewManager { let url = if PROXY_DEV_SERVER { Cow::Owned(Url::parse("tauri://localhost").unwrap()) } else { - app_manager.get_url() + app_manager.get_url(pending.webview_attributes.use_https_scheme) }; // ignore "index.html" just to simplify the url if path.to_str() != Some("index.html") { @@ -425,7 +435,7 @@ impl WebviewManager { } } WebviewUrl::External(url) => { - let config_url = app_manager.get_url(); + let config_url = app_manager.get_url(pending.webview_attributes.use_https_scheme); let is_local = config_url.make_relative(url).is_some(); let mut url = url.clone(); if is_local && PROXY_DEV_SERVER { @@ -572,8 +582,9 @@ impl WebviewManager { &self, window: Window, webview: DetachedWebview, + use_https_scheme: bool, ) -> Webview { - let webview = Webview::new(window, webview); + let webview = Webview::new(window, webview, use_https_scheme); let webview_event_listeners = self.event_listeners.clone(); let webview_ = webview.clone(); diff --git a/crates/tauri/src/pattern.rs b/crates/tauri/src/pattern.rs index 221fce9fb1c2..d0cd132c21b9 100644 --- a/crates/tauri/src/pattern.rs +++ b/crates/tauri/src/pattern.rs @@ -85,9 +85,10 @@ pub(crate) struct PatternJavascript { } #[allow(dead_code)] -pub(crate) fn format_real_schema(schema: &str) -> String { +pub(crate) fn format_real_schema(schema: &str, https: bool) -> String { if cfg!(windows) || cfg!(target_os = "android") { - format!("http://{schema}.{ISOLATION_IFRAME_SRC_DOMAIN}") + let scheme = if https { "https" } else { "http" }; + format!("{scheme}://{schema}.{ISOLATION_IFRAME_SRC_DOMAIN}") } else { format!("{schema}://{ISOLATION_IFRAME_SRC_DOMAIN}") } diff --git a/crates/tauri/src/protocol/isolation.rs b/crates/tauri/src/protocol/isolation.rs index 6d546d288695..f85f8e6cd3a7 100644 --- a/crates/tauri/src/protocol/isolation.rs +++ b/crates/tauri/src/protocol/isolation.rs @@ -21,9 +21,11 @@ pub fn get( assets: Arc, aes_gcm_key: [u8; 32], window_origin: String, + use_https_scheme: bool, ) -> UriSchemeProtocolHandler { let frame_src = if cfg!(any(windows, target_os = "android")) { - format!("http://{schema}.localhost") + let https = if use_https_scheme { "https" } else { "http" }; + format!("{https}://{schema}.localhost") } else { format!("{schema}:") }; diff --git a/crates/tauri/src/protocol/tauri.rs b/crates/tauri/src/protocol/tauri.rs index 2d33c4a00f7f..ac33b5be2de7 100644 --- a/crates/tauri/src/protocol/tauri.rs +++ b/crates/tauri/src/protocol/tauri.rs @@ -30,7 +30,10 @@ pub fn get( ) -> UriSchemeProtocolHandler { #[cfg(all(dev, mobile))] let url = { - let mut url = manager.get_url().as_str().to_string(); + let mut url = manager + .get_url(window_origin.starts_with("https")) + .as_str() + .to_string(); if url.ends_with('/') { url.pop(); } @@ -152,7 +155,8 @@ fn get_response( #[cfg(not(all(dev, mobile)))] let mut response = { - let asset = manager.get_asset(path)?; + let use_https_scheme = request.uri().scheme() == Some(&http::uri::Scheme::HTTPS); + let asset = manager.get_asset(path, use_https_scheme)?; builder = builder.header(CONTENT_TYPE, &asset.mime_type); if let Some(csp) = &asset.csp_header { builder = builder.header("Content-Security-Policy", csp); diff --git a/crates/tauri/src/test/mock_runtime.rs b/crates/tauri/src/test/mock_runtime.rs index 80c32e26d8ed..7f51b1902077 100644 --- a/crates/tauri/src/test/mock_runtime.rs +++ b/crates/tauri/src/test/mock_runtime.rs @@ -9,8 +9,10 @@ use tauri_runtime::{ dpi::{PhysicalPosition, PhysicalSize, Position, Size}, monitor::Monitor, webview::{DetachedWebview, PendingWebview}, - window::{CursorIcon, DetachedWindow, PendingWindow, RawWindow, WindowEvent, WindowId}, - window::{WindowBuilder, WindowBuilderBase}, + window::{ + CursorIcon, DetachedWindow, DetachedWindowWebview, PendingWindow, RawWindow, WindowBuilder, + WindowBuilderBase, WindowEvent, WindowId, + }, DeviceEventFilter, Error, EventLoopProxy, ExitRequestedEventAction, Icon, ProgressBarState, Result, RunEvent, Runtime, RuntimeHandle, RuntimeInitArgs, UserAttentionType, UserEvent, WebviewDispatch, WindowDispatch, WindowEventId, @@ -158,14 +160,17 @@ impl RuntimeHandle for MockRuntimeHandle { }, ); - let webview = webview_id.map(|id| DetachedWebview { - label: pending.label.clone(), - dispatcher: MockWebviewDispatcher { - id, - context: self.context.clone(), - url: Arc::new(Mutex::new(pending.webview.unwrap().url)), - last_evaluated_script: Default::default(), + let webview = webview_id.map(|id| DetachedWindowWebview { + webview: DetachedWebview { + label: pending.label.clone(), + dispatcher: MockWebviewDispatcher { + id, + context: self.context.clone(), + url: Arc::new(Mutex::new(pending.webview.unwrap().url)), + last_evaluated_script: Default::default(), + }, }, + use_https_scheme: false, }); Ok(DetachedWindow { @@ -773,14 +778,17 @@ impl WindowDispatch for MockWindowDispatcher { }, ); - let webview = webview_id.map(|id| DetachedWebview { - label: pending.label.clone(), - dispatcher: MockWebviewDispatcher { - id, - context: self.context.clone(), - url: Arc::new(Mutex::new(pending.webview.unwrap().url)), - last_evaluated_script: Default::default(), + let webview = webview_id.map(|id| DetachedWindowWebview { + webview: DetachedWebview { + label: pending.label.clone(), + dispatcher: MockWebviewDispatcher { + id, + context: self.context.clone(), + url: Arc::new(Mutex::new(pending.webview.unwrap().url)), + last_evaluated_script: Default::default(), + }, }, + use_https_scheme: false, }); Ok(DetachedWindow { @@ -1065,14 +1073,17 @@ impl Runtime for MockRuntime { }, ); - let webview = webview_id.map(|id| DetachedWebview { - label: pending.label.clone(), - dispatcher: MockWebviewDispatcher { - id, - context: self.context.clone(), - url: Arc::new(Mutex::new(pending.webview.unwrap().url)), - last_evaluated_script: Default::default(), + let webview = webview_id.map(|id| DetachedWindowWebview { + webview: DetachedWebview { + label: pending.label.clone(), + dispatcher: MockWebviewDispatcher { + id, + context: self.context.clone(), + url: Arc::new(Mutex::new(pending.webview.unwrap().url)), + last_evaluated_script: Default::default(), + }, }, + use_https_scheme: false, }); Ok(DetachedWindow { diff --git a/crates/tauri/src/webview/mod.rs b/crates/tauri/src/webview/mod.rs index 06fa0eda0909..199d5a8f90d8 100644 --- a/crates/tauri/src/webview/mod.rs +++ b/crates/tauri/src/webview/mod.rs @@ -605,11 +605,17 @@ tauri::Builder::default() pending.webview_attributes.bounds = Some(tauri_runtime::Rect { size, position }); + let use_https_scheme = pending.webview_attributes.use_https_scheme; + let webview = match &mut window.runtime() { RuntimeOrDispatch::Dispatch(dispatcher) => dispatcher.create_webview(pending), _ => unimplemented!(), } - .map(|webview| app_manager.webview.attach_webview(window.clone(), webview))?; + .map(|webview| { + app_manager + .webview + .attach_webview(window.clone(), webview, use_https_scheme) + })?; Ok(webview) } @@ -794,6 +800,21 @@ fn main() { self.webview_attributes.browser_extensions_enabled = enabled; self } + + /// Sets whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android. Defaults to `false`. + /// + /// ## Note + /// + /// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `://localhost` protocols used on macOS and Linux. + /// + /// ## Warning + /// + /// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data. + #[must_use] + pub fn use_https_scheme(mut self, enabled: bool) -> Self { + self.webview_attributes.use_https_scheme = enabled; + self + } } /// Webview. @@ -806,6 +827,7 @@ pub struct Webview { pub(crate) manager: Arc>, pub(crate) app_handle: AppHandle, pub(crate) resources_table: Arc>, + use_https_scheme: bool, } impl std::fmt::Debug for Webview { @@ -813,6 +835,7 @@ impl std::fmt::Debug for Webview { f.debug_struct("Window") .field("window", &self.window.lock().unwrap()) .field("webview", &self.webview) + .field("use_https_scheme", &self.use_https_scheme) .finish() } } @@ -825,6 +848,7 @@ impl Clone for Webview { manager: self.manager.clone(), app_handle: self.app_handle.clone(), resources_table: self.resources_table.clone(), + use_https_scheme: self.use_https_scheme, } } } @@ -847,13 +871,18 @@ impl PartialEq for Webview { /// Base webview functions. impl Webview { /// Create a new webview that is attached to the window. - pub(crate) fn new(window: Window, webview: DetachedWebview) -> Self { + pub(crate) fn new( + window: Window, + webview: DetachedWebview, + use_https_scheme: bool, + ) -> Self { Self { manager: window.manager.clone(), app_handle: window.app_handle.clone(), window: Arc::new(Mutex::new(window)), webview, resources_table: Default::default(), + use_https_scheme, } } @@ -880,6 +909,11 @@ impl Webview { &self.webview.label } + /// Whether the webview was configured to use the HTTPS scheme or not. + pub(crate) fn use_https_scheme(&self) -> bool { + self.use_https_scheme + } + /// Registers a window event listener. pub fn on_webview_event(&self, f: F) { self @@ -1180,9 +1214,11 @@ fn main() { } fn is_local_url(&self, current_url: &Url) -> bool { + let uses_https = current_url.scheme() == "https"; + // if from `tauri://` custom protocol ({ - let protocol_url = self.manager().protocol_url(); + let protocol_url = self.manager().protocol_url(uses_https); current_url.scheme() == protocol_url.scheme() && current_url.domain() == protocol_url.domain() }) || @@ -1190,7 +1226,7 @@ fn main() { // or if relative to `devUrl` or `frontendDist` self .manager() - .get_url() + .get_url(uses_https) .make_relative(current_url) .is_some() @@ -1206,7 +1242,7 @@ fn main() { // so we check using the first part of the domain #[cfg(any(windows, target_os = "android"))] let local = { - let protocol_url = self.manager().protocol_url(); + let protocol_url = self.manager().protocol_url(uses_https); let maybe_protocol = current_url .domain() .and_then(|d| d .split_once('.')) diff --git a/crates/tauri/src/webview/webview_window.rs b/crates/tauri/src/webview/webview_window.rs index 853d5406452d..f66b119abe19 100644 --- a/crates/tauri/src/webview/webview_window.rs +++ b/crates/tauri/src/webview/webview_window.rs @@ -898,6 +898,21 @@ impl<'a, R: Runtime, M: Manager> WebviewWindowBuilder<'a, R, M> { self.webview_builder = self.webview_builder.browser_extensions_enabled(enabled); self } + + /// Sets whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android. Defaults to `false`. + /// + /// ## Note + /// + /// Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `://localhost` protocols used on macOS and Linux. + /// + /// ## Warning + /// + /// Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access the old data. + #[must_use] + pub fn use_https_scheme(mut self, enabled: bool) -> Self { + self.webview_builder = self.webview_builder.use_https_scheme(enabled); + self + } } /// A type that wraps a [`Window`] together with a [`Webview`]. diff --git a/crates/tauri/src/window/mod.rs b/crates/tauri/src/window/mod.rs index 9b8e02b8ef7a..f1292b37ab94 100644 --- a/crates/tauri/src/window/mod.rs +++ b/crates/tauri/src/window/mod.rs @@ -381,7 +381,11 @@ tauri::Builder::default() ); if let Some(webview) = detached_window.webview { - app_manager.webview.attach_webview(window.clone(), webview); + app_manager.webview.attach_webview( + window.clone(), + webview.webview, + webview.use_https_scheme, + ); } window diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs index 512f43c0aac0..7f7007eef558 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -66,7 +66,8 @@ pub fn run_app) + Send + 'static>( .build()?, )); - let mut window_builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default()); + let mut window_builder = + WebviewWindowBuilder::new(app, "main", WebviewUrl::default()).use_https_scheme(true); #[cfg(all(desktop, not(test)))] { diff --git a/packages/api/src/webview.ts b/packages/api/src/webview.ts index e04252a95018..a495e135c2c2 100644 --- a/packages/api/src/webview.ts +++ b/packages/api/src/webview.ts @@ -734,6 +734,21 @@ interface WebviewOptions { * - **Android / iOS**: Unsupported. */ zoomHotkeysEnabled?: boolean + + /** + * Sets whether the custom protocols should use `https://.localhost` instead of the default `http://.localhost` on Windows and Android. Defaults to `false`. + * + * #### Note + * + * Using a `https` scheme will NOT allow mixed content when trying to fetch `http` endpoints and therefore will not match the behavior of the `://localhost` protocols used on macOS and Linux. + * + * #### Warning + * + * Changing this value between releases will change the IndexedDB, cookies and localstorage location and your app will not be able to access them. + * + * @since 2.1.0 + */ + useHttpsScheme?: boolean } export { Webview, getCurrentWebview, getAllWebviews } From 4191a7a53d941b179780a550638f1b4a09d17fd1 Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Tue, 5 Nov 2024 17:42:08 +0200 Subject: [PATCH 3/3] fix(tray): build tray on main thread (#11583) --- .changes/tray-async-command.md | 5 +++++ crates/tauri/src/lib.rs | 9 +++++++++ crates/tauri/src/menu/mod.rs | 7 ++----- crates/tauri/src/tray/mod.rs | 19 +++++++++++++++++-- 4 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 .changes/tray-async-command.md diff --git a/.changes/tray-async-command.md b/.changes/tray-async-command.md new file mode 100644 index 000000000000..9e40178190e2 --- /dev/null +++ b/.changes/tray-async-command.md @@ -0,0 +1,5 @@ +--- +"tauri": "patch:bug" +--- + +Fix tray events not fired for tray icons created inside an async command. diff --git a/crates/tauri/src/lib.rs b/crates/tauri/src/lib.rs index ee9f58f9a517..e37df1624d07 100644 --- a/crates/tauri/src/lib.rs +++ b/crates/tauri/src/lib.rs @@ -1011,6 +1011,15 @@ pub(crate) mod sealed { } } +struct UnsafeSend(T); +unsafe impl Send for UnsafeSend {} + +impl UnsafeSend { + fn take(self) -> T { + self.0 + } +} + #[allow(unused)] macro_rules! run_main_thread { ($handle:ident, $ex:expr) => {{ diff --git a/crates/tauri/src/menu/mod.rs b/crates/tauri/src/menu/mod.rs index 2797a5543ea2..2ae33d971b89 100644 --- a/crates/tauri/src/menu/mod.rs +++ b/crates/tauri/src/menu/mod.rs @@ -75,7 +75,6 @@ macro_rules! gen_wrappers { app_handle: $crate::AppHandle, } - /// # Safety /// /// We make sure it always runs on the main thread. @@ -96,11 +95,9 @@ macro_rules! gen_wrappers { impl Drop for $inner { fn drop(&mut self) { - struct SafeSend(T); - unsafe impl Send for SafeSend {} - let inner = self.inner.take(); - let inner = SafeSend(inner); + // SAFETY: inner was created on main thread and is being dropped on main thread + let inner = $crate::UnsafeSend(inner); let _ = self.app_handle.run_on_main_thread(move || { drop(inner); }); diff --git a/crates/tauri/src/tray/mod.rs b/crates/tauri/src/tray/mod.rs index 21623ff2b7d5..5c053f52b66d 100644 --- a/crates/tauri/src/tray/mod.rs +++ b/crates/tauri/src/tray/mod.rs @@ -10,6 +10,7 @@ use crate::app::{GlobalMenuEventListener, GlobalTrayIconEventListener}; use crate::menu::ContextMenu; use crate::menu::MenuEvent; use crate::resources::Resource; +use crate::UnsafeSend; use crate::{ image::Image, menu::run_item_main_thread, AppHandle, Manager, PhysicalPosition, Rect, Runtime, }; @@ -342,10 +343,24 @@ impl TrayIconBuilder { /// Builds and adds a new [`TrayIcon`] to the system tray. pub fn build>(self, manager: &M) -> crate::Result> { let id = self.id().clone(); - let inner = self.inner.build()?; + + // SAFETY: + // the menu within this builder was created on main thread + // and will be accessed on the main thread + let unsafe_builder = UnsafeSend(self.inner); + + let (tx, rx) = std::sync::mpsc::channel(); + let unsafe_tray = manager + .app_handle() + .run_on_main_thread(move || { + // SAFETY: will only be accessed on main thread + let _ = tx.send(unsafe_builder.take().build().map(UnsafeSend)); + }) + .and_then(|_| rx.recv().map_err(|_| crate::Error::FailedToReceiveMessage))??; + let icon = TrayIcon { id, - inner, + inner: unsafe_tray.take(), app_handle: manager.app_handle().clone(), };