From 379b9597fd8f905c5c296d8c78123756e038b2c2 Mon Sep 17 00:00:00 2001 From: Rodrigo Yokota Date: Tue, 11 Aug 2026 18:28:40 -0400 Subject: [PATCH] fix(cef): isolate embedded browser close on macOS --- .../src/cef_impl/client/life_span.rs | 78 ++++++++++++++++++- .../src/cef_impl/client/mod.rs | 4 +- .../src/platform/macos/webview.rs | 4 + crates/tauri-runtime-cef/src/runtime.rs | 14 ++++ crates/tauri-runtime-cef/src/webview.rs | 1 + 5 files changed, 99 insertions(+), 2 deletions(-) diff --git a/crates/tauri-runtime-cef/src/cef_impl/client/life_span.rs b/crates/tauri-runtime-cef/src/cef_impl/client/life_span.rs index dfae5fa463ed..2e3019a65b05 100644 --- a/crates/tauri-runtime-cef/src/cef_impl/client/life_span.rs +++ b/crates/tauri-runtime-cef/src/cef_impl/client/life_span.rs @@ -2,7 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::sync::{Arc, mpsc::Sender}; +use std::sync::{ + Arc, + atomic::{AtomicI32, Ordering}, + mpsc::Sender, +}; use cef::*; use tauri_runtime::{ @@ -14,6 +18,21 @@ use winit::event_loop::EventLoopProxy as WinitEventLoopProxy; use crate::runtime::{CefRuntime, Message, NewWindowOpener, RuntimeContext}; +const UNREGISTERED_BROWSER_ID: i32 = -1; + +fn register_primary_browser(primary_browser_id: &AtomicI32, browser_id: i32) { + let _ = primary_browser_id.compare_exchange( + UNREGISTERED_BROWSER_ID, + browser_id, + Ordering::AcqRel, + Ordering::Acquire, + ); +} + +fn should_isolate_browser_close(primary_browser_id: &AtomicI32, browser_id: i32) -> bool { + primary_browser_id.load(Ordering::Acquire) == browser_id +} + // There is some race condition on CEF that causes the app loading to fail // when there is a network service crash: // "[85296:47750637:0127/131203.017395:ERROR:content/browser/network_service_instance_impl.cc:610] Network service crashed or was terminated, restarting service." @@ -52,6 +71,7 @@ wrap_life_span_handler! { proxy: WinitEventLoopProxy, window_id: WindowId, webview_id: u32, + primary_browser_id: Arc, context: RuntimeContext, new_window_handler: Option>>>, initial_url: Option, @@ -59,6 +79,10 @@ wrap_life_span_handler! { impl LifeSpanHandler { fn on_after_created(&self, browser: Option<&mut Browser>) { + if let Some(browser) = browser.as_deref() { + register_primary_browser(&self.primary_browser_id, browser.identifier()); + } + if let Some(browser) = browser && let Some(initial_url) = &self.initial_url { @@ -66,6 +90,30 @@ wrap_life_span_handler! { } } + #[cfg(target_os = "macos")] + fn do_close(&self, browser: Option<&mut Browser>) -> std::os::raw::c_int { + let Some(browser) = browser else { + return 0; + }; + if !should_isolate_browser_close(&self.primary_browser_id, browser.identifier()) { + return 0; + } + + if self + .sender + .send(Message::BrowserCloseReady(self.webview_id)) + .is_err() + { + return 0; + } + self.proxy.wake_up(); + + // CEF's default result sends performClose:/WM_CLOSE/delete_event to the + // top-level parent. Tauri owns that window, so the runtime tears down + // only this browser's native child view instead. + 1 + } + fn on_before_popup( &self, _browser: Option<&mut Browser>, @@ -136,3 +184,31 @@ wrap_life_span_handler! { } } } + +#[cfg(test)] +mod tests { + use std::sync::atomic::AtomicI32; + + use super::{register_primary_browser, should_isolate_browser_close}; + + #[test] + fn isolates_only_the_runtime_owned_child_browser() { + let primary_browser_id = AtomicI32::new(-1); + + register_primary_browser(&primary_browser_id, 41); + + assert!(should_isolate_browser_close(&primary_browser_id, 41)); + assert!(!should_isolate_browser_close(&primary_browser_id, 42)); + } + + #[test] + fn popup_creation_does_not_replace_the_primary_browser() { + let primary_browser_id = AtomicI32::new(-1); + + register_primary_browser(&primary_browser_id, 41); + register_primary_browser(&primary_browser_id, 42); + + assert!(should_isolate_browser_close(&primary_browser_id, 41)); + assert!(!should_isolate_browser_close(&primary_browser_id, 42)); + } +} diff --git a/crates/tauri-runtime-cef/src/cef_impl/client/mod.rs b/crates/tauri-runtime-cef/src/cef_impl/client/mod.rs index 6ddfeb915024..aede02ba3958 100644 --- a/crates/tauri-runtime-cef/src/cef_impl/client/mod.rs +++ b/crates/tauri-runtime-cef/src/cef_impl/client/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::sync::{Arc, Mutex, mpsc::Sender}; +use std::sync::{Arc, Mutex, atomic::AtomicI32, mpsc::Sender}; use cef::*; use tauri_runtime::{UserEvent, window::WindowId}; @@ -71,6 +71,7 @@ wrap_client! { pub(crate) window_id: WindowId, pub(crate) webview_id: u32, pub(crate) label: String, + primary_browser_id: Arc, initial_url: Option, devtools_enabled: bool, drag_drop_event_target: DragDropEventTarget, @@ -107,6 +108,7 @@ wrap_client! { self.proxy.clone(), self.window_id, self.webview_id, + self.primary_browser_id.clone(), self.context.clone(), self.handlers.new_window_handler.clone(), self.initial_url.clone(), diff --git a/crates/tauri-runtime-cef/src/platform/macos/webview.rs b/crates/tauri-runtime-cef/src/platform/macos/webview.rs index 43e1e0a2d11d..b2daaba4477f 100644 --- a/crates/tauri-runtime-cef/src/platform/macos/webview.rs +++ b/crates/tauri-runtime-cef/src/platform/macos/webview.rs @@ -72,6 +72,10 @@ impl AppWebview { nsview.setHidden(!visible); } + pub(crate) fn destroy_native_view(&self) { + self.nsview().removeFromSuperview(); + } + pub(crate) fn apply_physical_bounds(&self, scale: f64, x: i32, y: i32, width: i32, height: i32) { let nsview = self.nsview(); let Some(parent) = (unsafe { nsview.superview() }) else { diff --git a/crates/tauri-runtime-cef/src/runtime.rs b/crates/tauri-runtime-cef/src/runtime.rs index cb24af6bffcc..bde96af25c09 100644 --- a/crates/tauri-runtime-cef/src/runtime.rs +++ b/crates/tauri-runtime-cef/src/runtime.rs @@ -308,6 +308,8 @@ pub(crate) type AfterWindowCreationCallback = Box Fn(RawWindow<'a>) pub(crate) enum Message { EventLoop(EventLoopMessage), + #[cfg(target_os = "macos")] + BrowserCloseReady(u32), BrowserClosed(WindowId, u32), Opened(Vec), #[cfg(target_os = "macos")] @@ -501,6 +503,18 @@ impl WinitCefApp { fn handle_message(&mut self, event_loop: &dyn ActiveEventLoop, message: Message) { match message { Message::EventLoop(message) => self.handle_event_loop_message(event_loop, message), + #[cfg(target_os = "macos")] + Message::BrowserCloseReady(webview_id) => { + if let Some(child) = self + .state + .windows + .values() + .flat_map(|appwindow| &appwindow.children) + .find(|child| child.webview_id == webview_id) + { + child.destroy_native_view(); + } + } Message::BrowserClosed(_window_id, webview_id) => { // Standalone webview.close() keeps the child in state until this // callback, so cleanup happens here. Window/app teardown removes child diff --git a/crates/tauri-runtime-cef/src/webview.rs b/crates/tauri-runtime-cef/src/webview.rs index edbc10311549..28013e72e2c3 100644 --- a/crates/tauri-runtime-cef/src/webview.rs +++ b/crates/tauri-runtime-cef/src/webview.rs @@ -374,6 +374,7 @@ impl WinitCefApp { window_id, webview_id, pending.label.clone(), + Arc::new(AtomicI32::new(-1)), Some(pending.url.as_str().to_string()), devtools_enabled, drag_drop_event_target,