From 371dd75175e4d2ff1ff680938c75f2df2f5b7f85 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2026 09:24:28 +0000 Subject: [PATCH] chore: sync from tauri-apps/tauri feat/cef --- Cargo.toml | 27 +- src/cef_impl/client/display.rs | 4 +- src/cef_impl/client/download.rs | 2 +- src/cef_impl/client/life_span.rs | 74 ++++-- src/cef_impl/client/load.rs | 2 +- src/cef_impl/client/mod.rs | 18 +- src/cef_impl/client/permission.rs | 81 ++---- src/cef_impl/ipc.rs | 41 +-- src/cef_impl/request_context.rs | 21 -- src/cef_impl/request_handler.rs | 407 ++++++++---------------------- src/lib.rs | 21 -- src/platform/linux/mod.rs | 2 - src/platform/linux/utils.rs | 53 +--- src/platform/linux/webview.rs | 8 - src/platform/macos/webview.rs | 5 - src/platform/mod.rs | 2 +- src/platform/windows/webview.rs | 71 +++++- src/runtime.rs | 256 ++++++++----------- src/webview.rs | 132 ++++++---- src/window.rs | 14 +- src/window_builder.rs | 36 ++- 21 files changed, 504 insertions(+), 773 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1fccfbf..33eb526 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,21 +1,19 @@ [package] name = "tauri-runtime-cef" version = "0.1.0" -description = "CEF runtime for Tauri, ported from tauri feat/cef branch onto published crates." -authors = ["Tauri Programme within The Commons Conservancy", "byeongsu-hong"] -homepage = "https://github.com/SableClient/tauri-runtime-cef" -repository = "https://github.com/SableClient/tauri-runtime-cef" -categories = ["gui"] -license = "Apache-2.0 OR MIT" -edition = "2024" -rust-version = "1.88" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +categories.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true [dependencies] base64 = "0.22" +cef = { version = "=150.0.0", features = ["build-util", "linux-x11"] } # Not actually used directly, just locking it. -cef = { version = "=150.2.1", features = ["build-util", "linux-x11"] } -# Not actually used directly, just locking it. -cef-dll-sys = { version = "=150.2.1", default-features = false } +cef-dll-sys = { version = "=150.0.0", default-features = false } dirs = "6" dioxus-debug-cell = "0.1" http = "1" @@ -26,8 +24,8 @@ raw-window-handle = "0.6" serde = { version = "1", features = ["derive"] } serde_json = "1" sha2 = "0.10" -tauri-runtime = "2.11.2" -tauri-utils = { version = "2.9.2", features = [ +tauri-runtime = { version = "2.11.2", path = "../tauri-runtime" } +tauri-utils = { version = "2.9.2", path = "../tauri-utils", features = [ "html-manipulation", ] } url = "2" @@ -98,6 +96,3 @@ default = ["sandbox"] devtools = [] macos-private-api = ["tauri-runtime/macos-private-api"] sandbox = ["cef/sandbox"] - -[dev-dependencies] -tauri = { version = "2", default-features = false, features = ["test"] } diff --git a/src/cef_impl/client/display.rs b/src/cef_impl/client/display.rs index 0e4aa7c..d80eecf 100644 --- a/src/cef_impl/client/display.rs +++ b/src/cef_impl/client/display.rs @@ -10,8 +10,8 @@ use crate::webview::INITIAL_LOAD_URL; wrap_display_handler! { pub struct TauriCefDisplayHandler { - document_title_changed_handler: Option>, - address_changed_handler: Option>, + document_title_changed_handler: Option>, + address_changed_handler: Option>, } impl DisplayHandler { diff --git a/src/cef_impl/client/download.rs b/src/cef_impl/client/download.rs index 2275533..b6f9489 100644 --- a/src/cef_impl/client/download.rs +++ b/src/cef_impl/client/download.rs @@ -8,7 +8,7 @@ use cef::*; wrap_download_handler! { pub struct TauriCefDownloadHandler { - download_handler: Arc, + download_handler: Arc, } impl DownloadHandler { diff --git a/src/cef_impl/client/life_span.rs b/src/cef_impl/client/life_span.rs index 35de171..dfae5fa 100644 --- a/src/cef_impl/client/life_span.rs +++ b/src/cef_impl/client/life_span.rs @@ -5,10 +5,14 @@ use std::sync::{Arc, mpsc::Sender}; use cef::*; -use tauri_runtime::{UserEvent, window::WindowId}; +use tauri_runtime::{ + UserEvent, + dpi::{LogicalPosition, LogicalSize}, + window::WindowId, +}; use winit::event_loop::EventLoopProxy as WinitEventLoopProxy; -use crate::runtime::{Message, RuntimeContext}; +use crate::runtime::{CefRuntime, Message, NewWindowOpener, RuntimeContext}; // There is some race condition on CEF that causes the app loading to fail // when there is a network service crash: @@ -48,9 +52,8 @@ wrap_life_span_handler! { proxy: WinitEventLoopProxy, window_id: WindowId, webview_id: u32, - webview_label: String, context: RuntimeContext, - new_window_handler: Option>, + new_window_handler: Option>>>, initial_url: Option, } @@ -72,41 +75,60 @@ wrap_life_span_handler! { _target_frame_name: Option<&CefString>, _target_disposition: WindowOpenDisposition, _user_gesture: std::os::raw::c_int, - _popup_features: Option<&PopupFeatures>, + popup_features: Option<&PopupFeatures>, _window_info: Option<&mut WindowInfo>, _client: Option<&mut Option>, _settings: Option<&mut BrowserSettings>, _extra_info: Option<&mut Option>, _no_javascript_access: Option<&mut i32>, ) -> std::os::raw::c_int { - // Return value: 0 = allow the popup, 1 = cancel it. - // A crate-level popup policy (set_popup_policy) decides per URL/label - // when installed. - let url = target_url.map(|u| u.to_string()).unwrap_or_default(); - if let Some(allow) = crate::policy::popup_allowed(&crate::policy::PopupRequest { - webview_label: &self.webview_label, - url: &url, - }) { - return i32::from(!allow); + let Some(handler) = &self.new_window_handler else { + return 0; + }; + + let Some(target_url) = target_url else { + return 1; + }; + + let url_str = target_url.to_string(); + let Ok(url) = url::Url::parse(&url_str) else { + return 1; + }; + + // window.open() features are CSS pixels, which map to Tauri's logical units. + let size = popup_features.and_then(|features| { + (features.width_set != 0 && features.height_set != 0) + .then(|| LogicalSize::new(features.width as f64, features.height as f64)) + }); + let position = popup_features.and_then(|features| { + (features.x_set != 0 && features.y_set != 0) + .then(|| LogicalPosition::new(features.x as f64, features.y as f64)) + }); + let features = + tauri_runtime::webview::NewWindowFeatures::new(size, position, NewWindowOpener {}); + + match handler(url, features) { + tauri_runtime::webview::NewWindowResponse::Allow => 0, + tauri_runtime::webview::NewWindowResponse::Create { window_id } => { + // CEF cannot transplant a popup's contents into an existing + // browser, so cancel the popup and navigate the designated + // window's first webview to the URL instead — the closest + // equivalent of wry hosting the popup in that window's webview. + // Note `window.opener` is not linked to the new document. + let _ = self.context.send_message(Message::NavigateFirstWebview { + window_id, + url: url_str, + }); + 1 + } + tauri_runtime::webview::NewWindowResponse::Deny => 1, } - // ponytail: published tauri's new-window handler cannot be invoked from - // CEF — its NewWindowFeatures wraps a wry platform webview handle - // (webkit2gtk::WebView on Linux) that a CEF browser cannot construct. - // An installed handler therefore degrades to a popup deny (the - // verdict every current caller returns); no handler keeps CEF's native - // popup behavior. Revisit when upstream releases feat/cef's - // runtime-generic opener. - i32::from(self.new_window_handler.is_some()) } fn on_before_close(&self, browser: Option<&mut Browser>) { if browser.is_none() { return; } - // Any permission prompt still open over this webview can no longer be - // granted to — deny it rather than leave the callback (and the app's - // consent UI) hanging over a dead browser. - crate::policy::cancel_pending(&self.webview_label); let _ = self .sender .send(Message::BrowserClosed(self.window_id, self.webview_id)); diff --git a/src/cef_impl/client/load.rs b/src/cef_impl/client/load.rs index 54d56df..33e53c2 100644 --- a/src/cef_impl/client/load.rs +++ b/src/cef_impl/client/load.rs @@ -8,7 +8,7 @@ use cef::*; wrap_load_handler! { pub struct TauriCefLoadHandler { - on_page_load_handler: Option>, + on_page_load_handler: Option>, } impl LoadHandler { diff --git a/src/cef_impl/client/mod.rs b/src/cef_impl/client/mod.rs index b1993f2..6ddfeb9 100644 --- a/src/cef_impl/client/mod.rs +++ b/src/cef_impl/client/mod.rs @@ -10,7 +10,7 @@ use winit::event_loop::EventLoopProxy as WinitEventLoopProxy; use crate::{ cef_impl::{ipc, request_handler}, - runtime::{Message, RuntimeContext}, + runtime::{CefRuntime, Message, RuntimeContext}, }; mod context_menu; @@ -39,13 +39,14 @@ pub(crate) use process::TauriCefBrowserProcessHandler; pub(crate) struct TauriCefBrowserClientHandlers { pub(crate) ipc_handler: Option>>, - pub(crate) on_page_load_handler: Option>, + pub(crate) on_page_load_handler: Option>, pub(crate) document_title_changed_handler: - Option>, - pub(crate) navigation_handler: Option>, - pub(crate) address_changed_handler: Option>, - pub(crate) new_window_handler: Option>, - pub(crate) download_handler: Option>, + Option>, + pub(crate) navigation_handler: Option>, + pub(crate) address_changed_handler: Option>, + pub(crate) new_window_handler: + Option>>>, + pub(crate) download_handler: Option>, pub(crate) web_content_process_terminate_handler: Option>, } @@ -106,7 +107,6 @@ wrap_client! { self.proxy.clone(), self.window_id, self.webview_id, - self.label.clone(), self.context.clone(), self.handlers.new_window_handler.clone(), self.initial_url.clone(), @@ -143,7 +143,7 @@ wrap_client! { } fn permission_handler(&self) -> Option { - Some(TauriCefPermissionHandler::new(self.label.clone())) + Some(TauriCefPermissionHandler::new()) } fn on_process_message_received( diff --git a/src/cef_impl/client/permission.rs b/src/cef_impl/client/permission.rs index 5f0c018..74b436d 100644 --- a/src/cef_impl/client/permission.rs +++ b/src/cef_impl/client/permission.rs @@ -2,90 +2,51 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -//! Adapter from CEF's permission callbacks to the runtime-neutral policy in -//! [`crate::policy`]. -//! -//! Both handlers hand the policy an owned [`PermissionResponder`] holding a -//! reference-counted clone of the CEF callback, so a policy may answer now or -//! later (a native consent prompt) without the callback dying underneath it. -//! Every path — including a policy that panics its way out or drops the -//! responder — completes the callback exactly once, and only an explicit -//! verdict completes it with a grant. - -use cef::{rc::Rc as _, *}; - -use crate::policy::{self, RequestSource}; +use cef::*; wrap_permission_handler! { - pub struct TauriCefPermissionHandler { - webview_label: String, - } + pub struct TauriCefPermissionHandler {} impl PermissionHandler { fn on_request_media_access_permission( &self, _browser: Option<&mut Browser>, - frame: Option<&mut Frame>, - requesting_origin: Option<&CefString>, + _frame: Option<&mut Frame>, + _requesting_origin: Option<&CefString>, requested_permissions: u32, callback: Option<&mut MediaAccessCallback>, ) -> ::std::os::raw::c_int { let Some(callback) = callback else { return 0; }; - // Reference-counted clone: the callback outlives this stack frame when - // the policy defers to a prompt. - let callback = callback.clone(); - let origin = requesting_origin.map(|origin| origin.to_string()).unwrap_or_default(); - let is_main_frame = frame.map(|frame| frame.is_main() != 0); - policy::dispatch( - &self.webview_label, - &origin, - RequestSource::MediaAccess, - policy::media_kinds(requested_permissions), - is_main_frame, - move |granted| { - // getUserMedia requires the granted mask to equal the requested one - // (cef_media_access_callback_t::cont), so this is all or nothing. - callback.cont(if granted { - requested_permissions - } else { - cef::sys::cef_media_access_permission_types_t::CEF_MEDIA_PERMISSION_NONE as u32 - }); - }, - ); - 1 + // Allow microphone and camera when requested. + let allowed = requested_permissions + & (cef::sys::cef_media_access_permission_types_t::CEF_MEDIA_PERMISSION_DEVICE_AUDIO_CAPTURE + as u32 + | cef::sys::cef_media_access_permission_types_t::CEF_MEDIA_PERMISSION_DEVICE_VIDEO_CAPTURE + as u32); + if allowed != 0 { + callback.cont(requested_permissions); + return 1; + } + 0 } fn on_show_permission_prompt( &self, _browser: Option<&mut Browser>, _prompt_id: u64, - requesting_origin: Option<&CefString>, - requested_permissions: u32, + _requesting_origin: Option<&CefString>, + _requested_permissions: u32, callback: Option<&mut PermissionPromptCallback>, ) -> ::std::os::raw::c_int { let Some(callback) = callback else { return 0; }; - let callback = callback.clone(); - let origin = requesting_origin.map(|origin| origin.to_string()).unwrap_or_default(); - policy::dispatch( - &self.webview_label, - &origin, - RequestSource::Prompt, - policy::prompt_kinds(requested_permissions), - // CEF reports no frame for permission prompts — they are browser-scoped. - None, - move |granted| { - let result = if granted { - cef::sys::cef_permission_request_result_t::CEF_PERMISSION_RESULT_ACCEPT - } else { - cef::sys::cef_permission_request_result_t::CEF_PERMISSION_RESULT_DENY - }; - callback.cont(PermissionRequestResult::from(result)); - }, - ); + // Allow permission prompt (e.g. microphone/camera). + callback.cont(PermissionRequestResult::from( + cef::sys::cef_permission_request_result_t::CEF_PERMISSION_RESULT_ACCEPT, + )); 1 } } diff --git a/src/cef_impl/ipc.rs b/src/cef_impl/ipc.rs index 9a57c6d..3a477a6 100644 --- a/src/cef_impl/ipc.rs +++ b/src/cef_impl/ipc.rs @@ -152,38 +152,17 @@ pub(crate) fn on_process_message_received( let body = CefString::from(&args.string(1)).to_string(); if let Ok(request) = http::Request::builder().uri(url).body(body) { - let webview = DetachedWebview { - label: client.label.clone(), - dispatcher: CefWebviewDispatcher { - window_id: Arc::new(Mutex::new(client.window_id)), - webview_id: client.webview_id, - context: client.context.clone(), + handler( + DetachedWebview { + label: client.label.clone(), + dispatcher: CefWebviewDispatcher { + window_id: Arc::new(Mutex::new(client.window_id)), + webview_id: client.webview_id, + context: client.context.clone(), + }, }, - }; - // Run the handler through the event loop instead of inside this CEF - // callout. A sync tauri command that round-trips the loop (window - // creation, blocking getters) would otherwise self-deadlock whenever this - // callout runs on the main thread OUTSIDE a winit callback — no current - // dispatch is installed, so the round-trip queues a message the parked - // loop can never drain. That is the steady state on macOS, where CEF work - // is pumped from NSRunLoop timer callouts (huddle pop-out froze the whole - // browser process). Where a dispatch IS installed (Linux services CEF via - // glib inside winit callbacks), send_message degenerates to the same - // inline call as before. - // - // ThreadSafe: the handler Arc is not Sync, but it never actually crosses - // threads — this callout runs on the CEF UI thread (the runtime main - // thread), and Message::Task closures execute on that same thread. - let handler = crate::cef_impl::request_handler::ThreadSafe(handler.clone()); - if let Err(error) = client - .context - .send_message(crate::runtime::Message::Task(Box::new(move || { - (handler.into_owned())(webview, request); - }))) - { - // Only fails when the loop is gone (shutdown) — the invoke is moot then. - log::debug!("dropped webview IPC message: {error}"); - } + request, + ); } 1 } diff --git a/src/cef_impl/request_context.rs b/src/cef_impl/request_context.rs index af705ff..3a15c38 100644 --- a/src/cef_impl/request_context.rs +++ b/src/cef_impl/request_context.rs @@ -328,14 +328,6 @@ pub(crate) fn request_context_from_webview_attributes<'a>( let settings = RequestContextSettings { cache_path, - // Per-context settings do not inherit the global value, so an empty list - // here would silently drop custom-scheme cookie support configured - // through `CefConfig::cookieable_schemes`. - cookieable_schemes_list: crate::config::config() - .cookieable_schemes - .join(",") - .as_str() - .into(), ..Default::default() }; @@ -366,7 +358,6 @@ pub(crate) fn request_context_from_webview_attributes<'a>( if let Some(request_context) = request_context.as_ref() { for scheme in custom_schemes { - // Windows/Android-style form: `http(s)://.localhost/…`. request_context.register_scheme_handler_factory( Some(&custom_protocol_scheme.into()), Some(&format!("{scheme}.localhost").as_str().into()), @@ -375,18 +366,6 @@ pub(crate) fn request_context_from_webview_attributes<'a>( scheme.clone(), )), ); - // Native form published tauri emits on Linux/macOS: - // `://localhost/…`. The scheme itself is made known to - // Chromium in `on_register_custom_schemes` (crate config list); an - // empty domain filter matches every host on the scheme. - request_context.register_scheme_handler_factory( - Some(&scheme.as_str().into()), - None, - Some(&mut request_handler::UriSchemeHandlerFactory::new( - scheme_registry.clone(), - scheme.clone(), - )), - ); } } diff --git a/src/cef_impl/request_handler.rs b/src/cef_impl/request_handler.rs index 28e6ec1..f667f82 100644 --- a/src/cef_impl/request_handler.rs +++ b/src/cef_impl/request_handler.rs @@ -16,9 +16,11 @@ use http::{ header::{CONTENT_SECURITY_POLICY, CONTENT_TYPE, ORIGIN}, }; use kuchiki::NodeRef; -use tauri_runtime::{UserEvent, window::WindowId}; - -use crate::compat::{NavigationHandler, UriSchemeProtocolHandler}; +use tauri_runtime::{ + UserEvent, + webview::{NavigationHandler, UriSchemeProtocolHandler}, + window::WindowId, +}; use tauri_utils::{ config::{Csp, CspDirectiveSources}, html::{parse as parse_html, serialize_node}, @@ -28,24 +30,10 @@ use url::Url; use crate::{ cef_impl::client::{DragDropEventTarget, DragDropState, WebDragDropResourceRequestHandler}, runtime::RuntimeContext, - streaming::{self, InitiatorOrigin, ReadOutcome, StreamBody}, webview::{CefInitScript, INITIAL_LOAD_URL}, }; type HttpResponse = Arc>>>>>; - -/// The pull side of a streaming custom-scheme response, installed by -/// `process_request` when the request's scheme has a streaming handler. `head` -/// is the shared slot the handler's `StreamResponder` publishes status + -/// headers into; `body` is drained by `read()`. Wrapped in `Arc` (not -/// the buffered path's `RefCell`) because the producer thread's wake closure -/// re-enters `body` to deliver chunks asynchronously. -type StreamCell = Arc>>; - -struct StreamState { - head: Arc>>>, - body: StreamBody, -} pub(crate) type SchemeRegistry = Arc< Mutex< std::collections::HashMap< @@ -212,55 +200,6 @@ wrap_request_handler! { } } -/// Copy an `http` head onto CEF's `Response`, set `Cache-Control: no-store`, -/// derive the MIME from `Content-Type`, and mark the body length unknown -/// (`-1`). Shared by the buffered and streaming `response_headers` paths, which -/// differ only in the head's body type. -fn write_response_headers( - cef_response: &mut Response, - head: &http::Response, - response_length: Option<&mut i64>, - redirect_url: Option<&mut CefString>, -) { - cef_response.set_status(head.status().as_u16() as i32); - let mut content_type = None; - - // Apply via a multimap so REPEATED header names survive — `Set-Cookie` is - // the common one, and a page that sets two cookies in a single response must - // keep both. `set_header_by_name(.., overwrite=0)` per value silently drops - // the second (it only sets when the name is absent), so build the whole map - // and set it once. `http::HeaderMap`'s iterator yields (name, value) for - // every value, so duplicates come through naturally. - let mut map = CefStringMultimap::new(); - for (name, value) in head.headers() { - let Ok(value) = value.to_str() else { - continue; - }; - map.append(name.as_str(), value); - if name == CONTENT_TYPE { - content_type.replace(value.to_string()); - } - } - cef_response.set_header_map(Some(&mut map)); - - cef_response.set_header_by_name(Some(&"Cache-Control".into()), Some(&"no-store".into()), 1); - - let mime_type = content_type - .as_ref() - .and_then(|t| t.split(';').next()) - .map(str::trim) - .unwrap_or("text/plain"); - cef_response.set_mime_type(Some(&mime_type.into())); - - if let Some(length) = response_length { - *length = -1; - } - - if let Some(redirect_url) = redirect_url { - let _ = std::mem::take(redirect_url); - } -} - wrap_resource_handler! { pub struct WebResourceHandler { webview_label: String, @@ -276,9 +215,6 @@ wrap_resource_handler! { initiator_origin: Option, // we clone response to send it to the handler thread response: HttpResponse, - // Set only when this request's scheme has a registered streaming handler; - // `response` stays empty in that case and the two paths never mix. - stream: StreamCell, } impl ResourceHandler { @@ -293,50 +229,70 @@ wrap_resource_handler! { let url = CefString::from(&request.url()).to_string(); let url = Url::parse(&url).ok(); - let Some(url) = url else { return 0 }; - let scheme = url.scheme().to_string(); - - // Extraction shared by both paths — reads `request` before it is dropped. - let label = self.webview_label.clone(); - let data = read_request_body(request); - let mut headers = get_request_headers(request); - - // The renderer can issue an IPC request before its execution context is - // fully wired to the loader; in that window Chromium sends the request - // with `Origin: null` even though the document already has a real - // origin. Repair that from the initiating main frame's URL, which the - // browser process tracks reliably. - // - // ONLY a literal `null` is repaired — an ABSENT `Origin` is left absent. - // Absence is meaningful: a top-level navigation and a same-origin GET - // carry no `Origin` by design, and inventing one turns a navigation into - // what looks like a cross-origin call from the *previous* page — which a - // server's origin check then rightly refuses. A correct renderer-sent - // origin always wins. - if let Some(initiator_origin) = &self.initiator_origin - && headers - .get(ORIGIN) - .is_some_and(|value| value.as_bytes() == b"null") - && let Ok(value) = HeaderValue::from_str(initiator_origin) - { - headers.insert(ORIGIN, value); - } + if let Some(url) = url { + let callback = ThreadSafe(callback.clone()); + let response_store = ThreadSafe(self.response.clone()); + let initialization_scripts = self.initialization_scripts.clone(); + let responder = Box::new(move |response: http::Response>| { + let is_html = response + .headers() + .get(CONTENT_TYPE) + .and_then(|ct| ct.to_str().ok()) + .map(|ct| ct.to_lowercase().starts_with("text/html")) + .unwrap_or(false); + + let (parts, body) = response.into_parts(); + let body_bytes = body.into_owned(); + let body_bytes = if is_html { + inject_scripts_into_html_body(&body_bytes, &initialization_scripts) + .unwrap_or(body_bytes) + } else { + body_bytes + }; + + let mut response = http::Response::from_parts(parts, Cursor::new(body_bytes)); + + if let Some(csp) = response.headers_mut().get_mut(CONTENT_SECURITY_POLICY) { + let csp_string = csp.to_str().unwrap_or_default().to_string(); + let new_csp = + csp_inject_initialization_scripts_hashes(csp_string, &initialization_scripts); + if let Ok(new_csp) = HeaderValue::from_str(&new_csp) { + *csp = new_csp; + } + } + + response_store.into_owned().borrow_mut().replace(response); + + let callback = callback.into_owned(); + callback.cont(); + }); + + let label = self.webview_label.clone(); + let handler = self.handler.clone(); + + let data = read_request_body(request); + let mut headers = get_request_headers(request); + + // The renderer can issue an IPC request before its execution context is + // fully wired to the loader; in that window Chromium sends the request + // with `Origin: null` even though the document already has a real + // origin. Repair it from the initiating main frame's URL, which the + // browser process tracks reliably. Only done when the renderer sent no + // origin or a literal `null`, so a correct renderer-sent origin always + // wins. + if let Some(initiator_origin) = &self.initiator_origin { + let origin_missing_or_null = headers + .get(ORIGIN) + .map(|value| value.as_bytes() == b"null") + .unwrap_or(true); + if origin_missing_or_null && let Ok(value) = HeaderValue::from_str(initiator_origin) { + headers.insert(ORIGIN, value); + } + } + + let method_str = CefString::from(&request.method()).to_string(); + let method = http::Method::from_bytes(method_str.as_bytes()).unwrap_or(http::Method::GET); - let method_str = CefString::from(&request.method()).to_string(); - let method = http::Method::from_bytes(method_str.as_bytes()).unwrap_or(http::Method::GET); - - // Streaming path: the scheme registered a streaming handler. The handler - // publishes the head (which fires `callback.cont()`), then writes body - // chunks that `read()` drains. Init-script HTML injection is intentionally - // skipped — streaming bodies are never buffered or parsed. - if let Some(stream_handler) = streaming::streaming_handler_for(&scheme) { - let on_head = ThreadSafe(callback.clone()); - let (responder, head, body) = streaming::make_stream(Box::new(move || { - on_head.into_owned().cont(); - })); - *self.stream.lock().expect("stream slot poisoned") = Some(StreamState { head, body }); - - let initiator = InitiatorOrigin(self.initiator_origin.clone()); std::thread::spawn(move || { let mut http_request = http::Request::builder() .method(method) @@ -344,136 +300,25 @@ wrap_resource_handler! { .body(data) .unwrap(); *http_request.headers_mut() = headers; - http_request.extensions_mut().insert(initiator); - stream_handler(&label, http_request, responder); + // handler is Arc>, so we need to dereference to call it + (**handler)(&label, http_request, responder); }); - return 1; + 1 + } else { + 0 } - - // Buffered path: tauri's uri-scheme protocol handler produces one whole - // response, which `read()` streams out of a `Cursor`. - let callback = ThreadSafe(callback.clone()); - let response_store = ThreadSafe(self.response.clone()); - let initialization_scripts = self.initialization_scripts.clone(); - let responder = Box::new(move |response: http::Response>| { - let is_html = response - .headers() - .get(CONTENT_TYPE) - .and_then(|ct| ct.to_str().ok()) - .map(|ct| ct.to_lowercase().starts_with("text/html")) - .unwrap_or(false); - - let (parts, body) = response.into_parts(); - let body_bytes = body.into_owned(); - let body_bytes = if is_html { - inject_scripts_into_html_body(&body_bytes, &initialization_scripts).unwrap_or(body_bytes) - } else { - body_bytes - }; - - let mut response = http::Response::from_parts(parts, Cursor::new(body_bytes)); - - if let Some(csp) = response.headers_mut().get_mut(CONTENT_SECURITY_POLICY) { - let csp_string = csp.to_str().unwrap_or_default().to_string(); - let new_csp = - csp_inject_initialization_scripts_hashes(csp_string, &initialization_scripts); - if let Ok(new_csp) = HeaderValue::from_str(&new_csp) { - *csp = new_csp; - } - } - - response_store.into_owned().borrow_mut().replace(response); - - let callback = callback.into_owned(); - callback.cont(); - }); - - let handler = self.handler.clone(); - std::thread::spawn(move || { - let mut http_request = http::Request::builder() - .method(method) - .uri(url.as_str()) - .body(data) - .unwrap(); - *http_request.headers_mut() = headers; - // handler is Arc>, so we need to dereference to call it - (**handler)(&label, http_request, responder); - }); - 1 } - #[allow(clippy::not_unsafe_ptr_arg_deref)] fn read( &self, data_out: *mut u8, bytes_to_read: ::std::os::raw::c_int, bytes_read: Option<&mut ::std::os::raw::c_int>, - callback: Option<&mut ResourceReadCallback>, + _callback: Option<&mut ResourceReadCallback>, ) -> ::std::os::raw::c_int { let Ok(bytes_to_read) = usize::try_from(bytes_to_read) else { return 0; }; - - // Streaming path: drive the StreamBody. When a chunk is buffered we copy - // it synchronously; when the producer has not written yet we retain - // `data_out` + `callback`, park a wake, and return continue-with-0 — CEF's - // async read contract. The wake (fired by the next `StreamWriter::write` - // or writer drop, on the producer thread) copies the chunk into the - // retained buffer and calls `callback.cont(n)`; `cont(0)` signals EOF. - { - let mut guard = self.stream.lock().expect("stream slot poisoned"); - if let Some(state) = guard.as_mut() { - if bytes_to_read == 0 { - if let Some(bytes_read) = bytes_read { - *bytes_read = 0; - } - return 1; - } - let out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read) }; - let stream = self.stream.clone(); - let callback = callback.map(|callback| ThreadSafe(callback.clone())); - let retained = ThreadSafe((data_out, bytes_to_read)); - let wake = move || { - let (ptr, len) = retained.into_owned(); - let out = unsafe { std::slice::from_raw_parts_mut(ptr, len) }; - let mut guard = stream.lock().expect("stream slot poisoned"); - let count = match guard.as_mut() { - // The wake only fires after a real write or the writer's drop, so - // `Pending` cannot occur here; treat it (and `Done`) as EOF. - Some(state) => match state.body.read(out, || {}) { - ReadOutcome::Copied(count) => count as ::std::os::raw::c_int, - ReadOutcome::Pending | ReadOutcome::Done => 0, - }, - None => 0, - }; - if let Some(callback) = callback { - callback.into_owned().cont(count); - } - }; - return match state.body.read(out, wake) { - ReadOutcome::Copied(count) => { - if let Some(bytes_read) = bytes_read { - *bytes_read = count as ::std::os::raw::c_int; - } - 1 - } - ReadOutcome::Pending => { - if let Some(bytes_read) = bytes_read { - *bytes_read = 0; - } - 1 - } - ReadOutcome::Done => { - if let Some(bytes_read) = bytes_read { - *bytes_read = 0; - } - 0 - } - }; - } - } - - // Buffered path: copy out of the response `Cursor`. let data_out = unsafe { std::slice::from_raw_parts_mut(data_out, bytes_to_read) }; let count = self .response @@ -499,28 +344,42 @@ wrap_resource_handler! { response_length: Option<&mut i64>, redirect_url: Option<&mut CefString>, ) { - let Some(response) = response else { + let (Some(response), Some(response_data)) = (response, &*self.response.borrow()) else { return; }; - // Streaming path: publish the head the `StreamResponder` stored. By the - // time CEF calls this, the handler has already fired `callback.cont()`, so - // the head slot is populated. - { - let guard = self.stream.lock().expect("stream slot poisoned"); - if let Some(state) = guard.as_ref() { - if let Some(head) = state.head.lock().expect("stream head poisoned").as_ref() { - write_response_headers(response, head, response_length, redirect_url); - } - return; + response.set_status(response_data.status().as_u16() as i32); + let mut content_type = None; + + // Set response headers and remember the MIME type for CEF. + for (name, value) in response_data.headers() { + let Ok(value) = value.to_str() else { + continue; + }; + + response.set_header_by_name(Some(&name.as_str().into()), Some(&value.into()), 0); + + if name == CONTENT_TYPE { + content_type.replace(value.to_string()); } } - // Buffered path. - let Some(response_data) = &*self.response.borrow() else { - return; - }; - write_response_headers(response, response_data, response_length, redirect_url); + response.set_header_by_name(Some(&"Cache-Control".into()), Some(&"no-store".into()), 1); + + let mime_type = content_type + .as_ref() + .and_then(|t| t.split(';').next()) + .map(str::trim) + .unwrap_or("text/plain"); + response.set_mime_type(Some(&mime_type.into())); + + if let Some(length) = response_length { + *length = -1; + } + + if let Some(redirect_url) = redirect_url { + let _ = std::mem::take(redirect_url); + } } } } @@ -558,7 +417,8 @@ wrap_scheme_handler_factory! { .filter(|frame| frame.is_main() == 1) .map(|frame| CefString::from(&frame.url()).to_string()) .and_then(|url| Url::parse(&url).ok()) - .and_then(|url| tuple_origin(&url)); + .map(|url| url.origin().ascii_serialization()) + .filter(|origin| origin != "null"); Some(WebResourceHandler::new( webview_label, @@ -566,16 +426,15 @@ wrap_scheme_handler_factory! { initialization_scripts, initiator_origin, Arc::new(RefCell::new(None)), - Arc::new(Mutex::new(None)), )) } } } -pub(crate) struct ThreadSafe(pub(crate) T); +struct ThreadSafe(T); impl ThreadSafe { - pub(crate) fn into_owned(self) -> T { + fn into_owned(self) -> T { self.0 } } @@ -623,56 +482,6 @@ fn read_request_body(request: &mut Request) -> Vec { body } -/// The tuple origin of `url`, as **Chromium** serializes it. -/// -/// `Url::origin()` implements the URL spec, where only *special* schemes -/// (http, https, ws, wss, ftp, file) get a tuple origin and everything else is -/// opaque — serialized `"null"`. But a custom scheme registered with -/// `CEF_SCHEME_OPTION_STANDARD` (which is every scheme in -/// `CefConfig::custom_schemes`, see `register_tauri_schemes`) DOES get a real -/// `scheme://host[:port]` tuple origin inside Chromium, and that is the origin -/// the renderer actually enforces (CSP, CORS) — and the one a scheme handler -/// must compare against. Composing it by hand for the opaque case is what -/// makes `InitiatorOrigin` usable on custom schemes at all; without this it is -/// always `None` there, silently disabling both the `Origin: null` repair in -/// `process_request` and any same-origin check a handler builds on it. -fn tuple_origin(url: &Url) -> Option { - let spec_origin = url.origin().ascii_serialization(); - if spec_origin != "null" { - return Some(spec_origin); - } - let host = url.host_str()?; - Some(match url.port() { - Some(port) => format!("{}://{}:{}", url.scheme(), host, port), - None => format!("{}://{}", url.scheme(), host), - }) -} - -#[cfg(test)] -mod tests { - use super::tuple_origin; - use url::Url; - - #[test] - fn tuple_origin_covers_special_and_custom_schemes() { - let special = Url::parse("https://example.com:8443/x?y").unwrap(); - assert_eq!( - tuple_origin(&special).as_deref(), - Some("https://example.com:8443") - ); - // A standard-registered custom scheme: the URL spec calls this opaque, but - // Chromium gives it a tuple origin, so we must too. - let custom = Url::parse("duck://site.alice.duck/index.html").unwrap(); - assert_eq!( - tuple_origin(&custom).as_deref(), - Some("duck://site.alice.duck") - ); - // Genuinely origin-less: nothing to compare against, so no origin. - let opaque = Url::parse("data:text/html,hi").unwrap(); - assert_eq!(tuple_origin(&opaque), None); - } -} - fn get_request_headers(request: &mut Request) -> HeaderMap { let mut headers = HeaderMap::new(); diff --git a/src/lib.rs b/src/lib.rs index 67137c7..29c5a55 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,36 +6,15 @@ #![allow(clippy::too_many_arguments)] mod cef_impl; -mod compat; -mod config; mod external_message_pump; mod platform; -mod policy; mod runtime; -mod streaming; mod webview; mod window; mod window_builder; mod window_handle; -pub use config::{CefConfig, configure}; -#[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" -))] -pub use platform::linux::install_x_error_handlers; -pub use policy::{ - DEFAULT_PROMPT_TIMEOUT, DeferredResponder, DenyReason, NormalizedOrigin, PermissionAudit, - PermissionKind, PermissionRequest, PermissionResponder, PopupRequest, RequestSource, Verdict, - set_permission_audit, set_permission_policy, set_popup_policy, -}; pub use runtime::*; -pub use streaming::{ - InitiatorOrigin, StreamClosed, StreamResponder, StreamWriter, register_streaming_scheme_handler, -}; pub use webview::*; pub use window::CefWindowDispatcher; pub use window_builder::WindowBuilderWrapper; diff --git a/src/platform/linux/mod.rs b/src/platform/linux/mod.rs index 3ce875b..f49c04e 100644 --- a/src/platform/linux/mod.rs +++ b/src/platform/linux/mod.rs @@ -8,5 +8,3 @@ mod taskbar; mod utils; mod webview; mod window; - -pub use utils::install_x_error_handlers; diff --git a/src/platform/linux/utils.rs b/src/platform/linux/utils.rs index 74e77ca..fe9b4e6 100644 --- a/src/platform/linux/utils.rs +++ b/src/platform/linux/utils.rs @@ -5,7 +5,7 @@ use std::{ cell::RefCell, ffi::CString, - os::raw::{c_int, c_long, c_ulong}, + os::raw::{c_long, c_ulong}, sync::LazyLock, }; use x11_dl::xlib; @@ -67,57 +67,6 @@ pub(super) fn with_x11(default: R, f: impl FnOnce(&xlib::Xlib, *mut xlib::Dis }) } -unsafe extern "C" fn x_error_handler( - _display: *mut xlib::Display, - event: *mut xlib::XErrorEvent, -) -> c_int { - if !event.is_null() { - let event = unsafe { &*event }; - log::warn!( - "X error received: type {}, serial {}, error_code {}, request_code {}, minor_code {}", - event.type_, - event.serial, - event.error_code, - event.request_code, - event.minor_code - ); - } - 0 -} - -unsafe extern "C" fn x_io_error_handler(_display: *mut xlib::Display) -> c_int { - log::error!("X IO error received: the display connection is gone"); - 0 -} - -/// Replace Xlib's process-killing default error handlers with logging no-ops. -/// -/// Xlib terminates the process on error by default: the stock error handler -/// prints and calls `exit(1)`, and the IO-error handler exits when the display -/// connection breaks. A non-fatal X protocol error — the kind a compositor or a -/// GPU reset produces on display resume — therefore takes the whole app down -/// with no Rust panic and no backtrace. -/// -/// Mirrors cefclient's `XErrorHandlerImpl`/`XIOErrorHandlerImpl`, installed -/// there for the same reason: -/// -/// -/// The runtime installs these itself after `cef::initialize`. **An embedder -/// that calls `gtk_init` must call this again afterwards**: GTK's X11 backend -/// installs its own handler during init, replacing whatever was there. This is -/// why cefclient installs its handlers *after* `gtk_init` rather than before. -/// Calling this more than once is harmless. -pub fn install_x_error_handlers() { - let Some(xlib) = XLIB.as_ref() else { - return; - }; - - unsafe { - (xlib.XSetErrorHandler)(Some(x_error_handler)); - (xlib.XSetIOErrorHandler)(Some(x_io_error_handler)); - } -} - pub(super) fn atom(xlib: &xlib::Xlib, display: *mut xlib::Display, name: &str) -> c_ulong { let cname = CString::new(name).unwrap(); unsafe { (xlib.XInternAtom)(display, cname.as_ptr(), 0) } diff --git a/src/platform/linux/webview.rs b/src/platform/linux/webview.rs index b32e629..410884c 100644 --- a/src/platform/linux/webview.rs +++ b/src/platform/linux/webview.rs @@ -105,14 +105,6 @@ impl AppWebview { }); } - pub(crate) fn destroy_native(&self) { - let xid = self.xid(); - with_cef_display((), |xlib, display| unsafe { - (xlib.XDestroyWindow)(display, xid); - (xlib.XFlush)(display); - }); - } - pub(crate) fn apply_physical_bounds(&self, _scale: f64, x: i32, y: i32, width: i32, height: i32) { let xid = self.xid(); diff --git a/src/platform/macos/webview.rs b/src/platform/macos/webview.rs index 0ff667e..43e1e0a 100644 --- a/src/platform/macos/webview.rs +++ b/src/platform/macos/webview.rs @@ -72,11 +72,6 @@ impl AppWebview { nsview.setHidden(!visible); } - pub(crate) fn destroy_native(&self) { - let nsview = self.nsview(); - 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/src/platform/mod.rs b/src/platform/mod.rs index 28f4127..ea35617 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -15,7 +15,7 @@ pub mod macos; target_os = "netbsd", target_os = "openbsd" ))] -pub mod linux; +mod linux; use tauri_runtime::dpi::PhysicalRect; use winit::monitor::MonitorHandle; diff --git a/src/platform/windows/webview.rs b/src/platform/windows/webview.rs index 02e4843..ffb4f95 100644 --- a/src/platform/windows/webview.rs +++ b/src/platform/windows/webview.rs @@ -6,11 +6,12 @@ use cef::ImplBrowserHost; use tauri_runtime::dpi::{PhysicalPosition, PhysicalSize, Rect}; use tauri_utils::config::Color; use windows::Win32::{ - Foundation::{HWND, POINT, RECT}, + Foundation::{HWND, LPARAM, LRESULT, POINT, RECT, WPARAM}, Graphics::Gdi::MapWindowPoints, + UI::Shell::{DefSubclassProc, SetWindowSubclass}, UI::WindowsAndMessaging::{ - DestroyWindow, GetParent, GetWindowRect, SW_HIDE, SW_SHOW, SWP_NOACTIVATE, SWP_NOZORDER, - SetParent, SetWindowPos, ShowWindow, + GetParent, GetWindowRect, HWND_TOP, SW_HIDE, SW_SHOW, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, + SWP_NOZORDER, SetParent, SetWindowPos, ShowWindow, WINDOWPOS, WM_WINDOWPOSCHANGING, }, }; @@ -72,8 +73,68 @@ impl AppWebview { let _ = unsafe { ShowWindow(self.hwnd(), if visible { SW_SHOW } else { SW_HIDE }) }; } - pub(crate) fn destroy_native(&self) { - let _ = unsafe { DestroyWindow(self.hwnd()) }; + const PIN_Z_ORDER_SUBCLASS_ID: usize = 124; + /// `dwRefData` of the pin subclass: whether it is currently vetoing. + const Z_ORDER_UNPINNED: usize = 0; + const Z_ORDER_PINNED: usize = 1; + + /// Refuses every z-order change to this webview while the pin is engaged. + unsafe extern "system" fn pin_z_order_subclass_proc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + _subclass_id: usize, + pinned: usize, + ) -> LRESULT { + unsafe { + if pinned == Self::Z_ORDER_PINNED && msg == WM_WINDOWPOSCHANGING && lparam.0 != 0 { + let window_pos = &mut *(lparam.0 as *mut WINDOWPOS); + window_pos.flags |= SWP_NOZORDER; + } + + DefSubclassProc(hwnd, msg, wparam, lparam) + } + } + + /// Engages or disengages the z-order pin. + /// + /// Re-installing the same proc under the same id does not chain a second + /// subclass, it just updates `dwRefData` — so this both installs the pin the + /// first time and toggles it afterwards. + fn set_z_order_pinned(&self, pinned: bool) { + let _ = unsafe { + SetWindowSubclass( + self.hwnd(), + Some(Self::pin_z_order_subclass_proc), + Self::PIN_Z_ORDER_SUBCLASS_ID, + if pinned { + Self::Z_ORDER_PINNED + } else { + Self::Z_ORDER_UNPINNED + }, + ) + }; + } + + /// Raises this webview above its siblings and pins it there, so nothing but + /// this runtime can move it again. See [`Self::pin_z_order_subclass_proc`]. + pub(crate) fn raise_to_top(&self) { + self.set_z_order_pinned(false); + + let _ = unsafe { + SetWindowPos( + self.hwnd(), + Some(HWND_TOP), + 0, + 0, + 0, + 0, + SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE, + ) + }; + + self.set_z_order_pinned(true); } pub(crate) fn apply_physical_bounds(&self, _scale: f64, x: i32, y: i32, width: i32, height: i32) { diff --git a/src/runtime.rs b/src/runtime.rs index 7d69489..421083f 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -44,7 +44,10 @@ use crate::external_message_pump::CefExternalPump; use crate::platform::EventLoopExt; use crate::{ cef_impl::{client as browser_client, ipc, request_handler}, - webview::{self, AppWebview, CefWebviewDispatcher, WebviewMessage, create_webview_detached}, + webview::{ + self, AppWebview, CefWebviewDispatcher, Webview, WebviewAtribute, WebviewMessage, + create_webview_detached, + }, window::{ AppWindow, CefWindowDispatcher, WindowMessage, create_window_detached, winit_monitor_to_tauri_monitor, winit_theme_to_tauri_theme, @@ -73,6 +76,54 @@ use winit::platform::x11::EventLoopBuilderExtX11; /// in minor releases when a known breaking change is discovered. pub use cef; +/// Platform-specific runtime init attributes. +#[derive(Clone, Debug)] +pub enum RuntimeInitAttribute { + /// Command line arguments passed to CEF. + CommandLineArgs { args: Vec<(String, Option)> }, + /// Deep link schemes. + DeepLinkSchemes { schemes: Vec }, + /// Directory used for CEF disk cache (`Settings::cache_path`). + /// + /// If unspecified, defaults to `{user cache}/{app identifier}/cef`. + CachePath { path: PathBuf }, +} + +impl tauri_runtime::InitAttribute for RuntimeInitAttribute { + fn new(config: &tauri_utils::config::Config) -> Result> { + let mut attrs = Vec::new(); + if let Some(plugin_config) = config + .plugins + .0 + .get("deep-link") + .and_then(|config| config.get("desktop").cloned()) + { + #[derive(serde::Deserialize)] + #[serde(untagged)] + enum DesktopDeepLinks { + One(tauri_utils::config::DeepLinkProtocol), + List(Vec), + } + + let protocols: DesktopDeepLinks = + serde_json::from_value(plugin_config).map_err(tauri_runtime::Error::Json)?; + let schemes = match protocols { + DesktopDeepLinks::One(protocol) => protocol.schemes, + DesktopDeepLinks::List(protocols) => protocols + .into_iter() + .flat_map(|protocol| protocol.schemes) + .collect(), + }; + + attrs.push(RuntimeInitAttribute::DeepLinkSchemes { schemes }); + } + Ok(attrs) + } +} + +#[derive(Debug)] +pub struct NewWindowOpener {} + #[derive(Clone, Debug)] pub struct EventProxy { context: RuntimeContext, @@ -289,6 +340,10 @@ pub(crate) enum Message { webview_id: u32, message: WebviewMessage, }, + NavigateFirstWebview { + window_id: WindowId, + url: String, + }, DragDropScriptEvent { window_id: WindowId, webview_id: u32, @@ -392,9 +447,6 @@ pub(crate) struct WinitCefApp { receiver: Receiver>, pub(crate) state: AppState, pub(crate) scheme_registry: request_handler::SchemeRegistry, - /// Exit code from `RequestExit`, read back by `Runtime::run_return` after - /// the event loop finishes (winit's `run_app` return carries no code). - exit_code: Arc, } impl WinitCefApp { @@ -403,7 +455,6 @@ impl WinitCefApp { receiver: Receiver>, callback: Box)>, scheme_registry: request_handler::SchemeRegistry, - exit_code: Arc, ) -> Self { Self { context, @@ -416,7 +467,6 @@ impl WinitCefApp { exiting: false, }, scheme_registry, - exit_code, } } @@ -508,6 +558,9 @@ impl WinitCefApp { webview_id, message, } => self.handle_webview_message(window_id, webview_id, message), + Message::NavigateFirstWebview { window_id, url } => { + self.navigate_first_webview(window_id, &url) + } Message::DragDropScriptEvent { window_id, webview_id, @@ -522,21 +575,11 @@ impl WinitCefApp { Message::Task(task) => task(), Message::RequestExit(code) => { if self.request_exit(Some(code)) { - self.exit_code.store(code, Ordering::Release); self.close_all_browsers(); self.exit_if_done(event_loop); } } - // Published tauri-runtime only has RunEvent::Opened on macOS/iOS/ - // Android; elsewhere the deep-link relaunch event has nowhere to go. - #[cfg(target_os = "macos")] Message::Opened(urls) => self.run_callback(RunEvent::Opened { urls }), - #[cfg(not(target_os = "macos"))] - Message::Opened(urls) => { - log::warn!( - "dropping deep-link open event {urls:?}: no RunEvent::Opened on this platform in published tauri-runtime" - ); - } #[cfg(target_os = "macos")] Message::Reopen { has_visible_windows, @@ -694,18 +737,6 @@ impl WinitCefApp { } pub(crate) fn close_window(&mut self, window_id: WindowId, event_loop: &dyn ActiveEventLoop) { - if !self.state.windows.contains_key(&window_id) { - return; - } - // Emit Destroyed while the window is still in state (emit_window_event - // needs it): tauri's core prunes its window registry on this event, and - // app close hooks rely on it. Without it every closed window lives on as - // a zombie label — get_webview_window keeps returning a dead handle. The - // winit Destroyed event can't cover this: by the time it fires the id - // mapping below is already gone, so it never routes back to this window. - if !self.state.exiting { - self.emit_window_event(window_id, WindowEvent::Destroyed); - } let Some(appwindow) = self.state.windows.remove(&window_id) else { return; }; @@ -760,6 +791,20 @@ impl WinitCefApp { } } + fn navigate_first_webview(&self, window_id: WindowId, url: &str) { + let Some(frame) = self + .state + .windows + .get(&window_id) + .and_then(|window| window.children.first()) + .and_then(|webview| webview.browser.main_frame()) + else { + return; + }; + + frame.load_url(Some(&CefString::from(url))); + } + fn close_all_browsers(&mut self) { // App shutdown follows the same eager bookkeeping cleanup as window // teardown. live_browsers keeps the loop alive until CEF confirms every @@ -800,10 +845,9 @@ impl WinitCefApp { } /// Service the default GLib main context so the external message pump's GLib - /// timeout (and any GTK work CEF schedules) gets dispatched, then arm winit to - /// wake when the next tick is due. CEF is driven by that timeout firing, not - /// from here. Windows/macOS need no equivalent: their pump timers live on the - /// native loop winit already runs. + /// source (and any GTK work CEF schedules) gets dispatched, then arm winit to + /// wake when the next GLib pump deadline is due. Windows/macOS need no + /// equivalent: their pump timers live on the native loop winit already runs. #[cfg(any( target_os = "linux", target_os = "dragonfly", @@ -878,8 +922,9 @@ impl ApplicationHandler for WinitCefApp { WinitWindowEvent::CloseRequested => self.request_window_close(window_id, event_loop), WinitWindowEvent::Destroyed => { - // close_window emits WindowEvent::Destroyed (exactly once — a window - // that already went through close_window no longer routes here). + if !self.state.exiting { + self.emit_window_event(window_id, WindowEvent::Destroyed); + } self.close_window(window_id, event_loop); } WinitWindowEvent::SurfaceResized(size) => { @@ -942,26 +987,6 @@ impl ApplicationHandler for WinitCefApp { } } -/// Registers the config-listed tauri custom protocol schemes with Chromium. -/// -/// Published tauri serves custom protocols at their native URL forms on -/// Linux/macOS (`tauri://localhost`, `ipc://localhost`, `asset://localhost`), -/// so Chromium must know each scheme as standard (URLs get an origin and -/// relative resolution), secure (secure-context APIs like WebCodecs and -/// getUserMedia work), CORS-enabled and fetch-enabled (the IPC transport is a -/// `fetch` POST to `ipc://localhost/`). Runs in every CEF process — the -/// helper re-exec path registers the same set via `TauriCefHelperApp`. -fn register_tauri_schemes(registrar: Option<&mut SchemeRegistrar>) { - let Some(registrar) = registrar else { return }; - let options = sys::cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32 - | sys::cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32 - | sys::cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32 - | sys::cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32; - for scheme in &crate::config::config().custom_schemes { - registrar.add_custom_scheme(Some(&CefString::from(scheme.as_str())), options); - } -} - wrap_app! { struct TauriCefApp { context: RuntimeContext, @@ -971,10 +996,6 @@ wrap_app! { } impl App { - fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { - register_tauri_schemes(registrar); - } - fn render_process_handler(&self) -> Option { Some(ipc::TauriRenderProcessHandler::new()) } @@ -1010,32 +1031,6 @@ wrap_app! { } } -/// Returns the pid of a verifiably-alive process holding this cache's -/// Chromium `SingletonLock`, if any. The lock is a symlink to -/// `-`; a stale lock (dead pid, or another host on a shared -/// home) is ignored — Chromium recovers those itself. -fn live_singleton_lock_holder(cache_path: &std::path::Path) -> Option { - let target = std::fs::read_link(cache_path.join("SingletonLock")).ok()?; - let target = target.to_string_lossy(); - let (host, pid) = target.rsplit_once('-')?; - let pid: u32 = pid.parse().ok()?; - let our_host = std::fs::read_to_string("/proc/sys/kernel/hostname") - .map(|h| h.trim().to_string()) - .unwrap_or_default(); - if !our_host.is_empty() && host != our_host { - return None; - } - #[cfg(target_os = "linux")] - let alive = std::path::Path::new(&format!("/proc/{pid}")).exists(); - #[cfg(not(target_os = "linux"))] - let alive = std::process::Command::new("kill") - .args(["-0", &pid.to_string()]) - .status() - .map(|s| s.success()) - .unwrap_or(false); - alive.then_some(pid) -} - pub fn run_cef_helper_process() { let args = cef::args::Args::new(); @@ -1066,10 +1061,6 @@ wrap_app! { struct TauriCefHelperApp; impl App { - fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { - register_tauri_schemes(registrar); - } - fn render_process_handler(&self) -> Option { Some(ipc::TauriRenderProcessHandler::new()) } @@ -1287,7 +1278,7 @@ impl TerminationSignals { impl CefRuntime { fn init( mut event_loop_builder: EventLoopBuilder, - #[allow(unused_variables)] runtime_args: RuntimeInitArgs, + runtime_args: RuntimeInitArgs, ) -> Result { // Snapshot before CEF can touch anything, so we can tell an embedder's own // signal policy apart from the handlers CEF installs in `cef::initialize`. @@ -1356,35 +1347,23 @@ impl CefRuntime { std::process::exit(ret.max(0)); } - // Published tauri's RuntimeInitArgs has no channel for CEF-specific init - // data (identifier/switches/cache path), so it comes from the - // process-global crate config instead — see `crate::configure`. - let cef_config = crate::config::config(); - let mut command_line_args = cef_config.command_line_args.clone(); - let deep_link_schemes = cef_config.deep_link_schemes.clone(); + let mut command_line_args = Vec::new(); + let mut deep_link_schemes = Vec::new(); + let mut cache_path_override = None::; + for arg in runtime_args.platform_specific_attributes { + match arg { + RuntimeInitAttribute::CommandLineArgs { args } => command_line_args.extend(args), + RuntimeInitAttribute::DeepLinkSchemes { schemes } => deep_link_schemes.extend(schemes), + RuntimeInitAttribute::CachePath { path } => cache_path_override = Some(path), + } + } - let cache_path = cef_config.cache_path.clone().unwrap_or_else(|| { + let cache_path = cache_path_override.unwrap_or_else(|| { let cache_base = dirs::cache_dir().unwrap_or_else(std::env::temp_dir); - cache_base.join(&cef_config.identifier).join("cef") + cache_base.join(&runtime_args.identifier).join("cef") }); let _ = create_dir_all(&cache_path); - // Chromium guards its profile with a `SingletonLock` symlink whose target - // is `-`. A second browser process on the same cache dir - // doesn't fail at initialize — Chromium only surfaces the conflict later, - // as a renderer/GPU startup failure. Fail fast with an actionable error - // instead when the holder is verifiably alive. - if let Some(holder_pid) = live_singleton_lock_holder(&cache_path) { - return Err(Error::CreateWebview( - format!( - "CEF cache {} is held by running process {holder_pid} (SingletonLock); \ - close that instance or configure a distinct cache_path/identifier", - cache_path.display() - ) - .into(), - )); - } - // Force X11 usage on Linux #[cfg(any( target_os = "linux", @@ -1428,13 +1407,7 @@ impl CefRuntime { cache_path: Arc::new(cache_path.clone()), }; - // NOT `--enable-media-stream`: CEF documents that switch as granting all - // media permissions, and it suppresses OnRequestMediaAccessPermission - // entirely ("This function will not be called if the --enable-media-stream - // command-line switch is used"). Every camera, microphone and screen - // request would bypass the permission policy — silently, since the handler - // never runs. Media access is gated like any other permission; an app that - // wants the blanket grant can set the switch itself through CefConfig. + command_line_args.push(("--enable-media-stream".to_string(), None)); let mut app = TauriCefApp::new( context.clone(), context_initialized.clone(), @@ -1458,9 +1431,6 @@ impl CefRuntime { no_sandbox: !cfg!(feature = "sandbox") as i32, cache_path: cache_path.to_string_lossy().to_string().as_str().into(), external_message_pump: 1, - // Comma-delimited; empty keeps CEF's http/https-only default. The - // defaults stay included because exclude_defaults is left 0. - cookieable_schemes_list: cef_config.cookieable_schemes.join(",").as_str().into(), ..Default::default() }; if cef::initialize( @@ -1482,18 +1452,6 @@ impl CefRuntime { ))] pre_cef_signals.restore(); - // Baseline for embedders that never touch GTK. One that calls `gtk_init` - // must call `install_x_error_handlers` again afterwards — GTK's X11 backend - // replaces the handler during init. - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "openbsd", - target_os = "netbsd" - ))] - crate::platform::linux::install_x_error_handlers(); - #[cfg(target_os = "macos")] let app_delegate = if !is_helper { use crate::platform::macos::AppDelegateEvent; @@ -1545,8 +1503,12 @@ impl Runtime for CefRuntime { type WebviewDispatcher = CefWebviewDispatcher; type Handle = CefRuntimeHandle; type EventLoopProxy = EventProxy; + type PlatformSpecificWebviewAttribute = WebviewAtribute; + type Webview = Webview; + type PlatformSpecificInitAttribute = RuntimeInitAttribute; + type WindowOpener = NewWindowOpener; - fn new(args: RuntimeInitArgs) -> Result { + fn new(args: RuntimeInitArgs) -> Result { Self::init(EventLoopBuilder::default(), args) } @@ -1558,7 +1520,7 @@ impl Runtime for CefRuntime { target_os = "netbsd", target_os = "openbsd" ))] - fn new_any_thread(args: RuntimeInitArgs) -> Result { + fn new_any_thread(args: RuntimeInitArgs) -> Result { let mut event_loop_builder = EventLoopBuilder::default(); event_loop_builder.with_any_thread(true); Self::init(event_loop_builder, args) @@ -1651,6 +1613,13 @@ impl Runtime for CefRuntime { .listen_device_events(device_event_filter_to_winit(filter)); } + fn custom_scheme_url(scheme: &str, https: bool) -> String { + format!( + "{}://{scheme}.localhost", + if https { "https" } else { "http" } + ) + } + fn run_iteration) + 'static>(&mut self, mut callback: F) { while let Ok(message) = self.receiver.try_recv() { if let Message::UserEvent(event) = message { @@ -1662,20 +1631,19 @@ impl Runtime for CefRuntime { } fn run_return) + 'static>(self, callback: F) -> i32 { - let exit_code = Arc::new(std::sync::atomic::AtomicI32::new(0)); + self.run(callback); + // TODO: return the exit code from the runtime, if possible. For now, always return 0 + 0 + } + + fn run) + 'static>(self, callback: F) { let app = WinitCefApp::new( self.context, self.receiver, Box::new(callback), self.scheme_registry, - exit_code.clone(), ); let _ = self.event_loop.run_app(app); cef::shutdown(); - exit_code.load(Ordering::Acquire) - } - - fn run) + 'static>(self, callback: F) { - self.run_return(callback); } } diff --git a/src/webview.rs b/src/webview.rs index ec86dc7..edbc103 100644 --- a/src/webview.rs +++ b/src/webview.rs @@ -13,16 +13,18 @@ use std::sync::{ use cef::*; use sha2::{Digest, Sha256}; use tauri_runtime::{ - Cookie, Error, Result, UserEvent, WebviewDispatch, WebviewEventId, + Cookie, Error, Result, Runtime, UserEvent, WebviewDispatch, WebviewEventId, dpi::{PhysicalPosition, PhysicalSize, Position, Rect, Size}, - webview::{DetachedWebview, InitializationScript, PendingWebview, WebviewAttributes}, + webview::{ + DetachedWebview, InitializationScript, PendingWebview, UriSchemeProtocolHandler, + WebviewAttributes, + }, window::{WebviewEvent, WindowId}, }; use tauri_utils::{Theme, config::Color, html::normalize_script_for_csp}; use url::Url; use crate::cef_impl::{client as browser_client, cookie, request_context, request_handler}; -use crate::compat::{self, UriSchemeProtocolHandler}; use crate::runtime::{CefRuntime, Message, RuntimeContext, WinitCefApp}; use crate::window::AppWindow; @@ -302,6 +304,13 @@ impl WinitCefApp { )); }; + // On Windows a window's webviews are sibling child HWNDs. Put each new one + // on top of the ones already there — the order they were created in — and + // pin it, so Chromium's focus raise cannot reshuffle them behind our back + // and bury an overlay webview under the one that fills the window. + #[cfg(windows)] + child.raise_to_top(); + *live_browsers += 1; appwindow.children.push(child); layout_app_window(appwindow); @@ -337,9 +346,7 @@ impl WinitCefApp { let on_page_load_handler = pending.on_page_load_handler.take().map(Arc::from); let document_title_changed_handler = pending.document_title_changed_handler.take().map(Arc::from); - // Published PendingWebview has no address-changed channel (feat/cef-only); - // the client plumbing stays for when upstream ships it. - let address_changed_handler: Option> = None; + let address_changed_handler = pending.address_changed_handler.take().map(Arc::from); let devtools_enabled = (cfg!(debug_assertions) || cfg!(feature = "devtools")) && pending.webview_attributes.devtools.unwrap_or(true); let drag_drop_handler_enabled = pending.webview_attributes.drag_drop_handler_enabled; @@ -384,9 +391,9 @@ impl WinitCefApp { size: parent_size.into(), }); #[cfg(not(target_os = "macos"))] - let bounds = compat::rect_to_physical::(bounds, scale); + let bounds = bounds.to_physical::(scale); #[cfg(target_os = "macos")] - let bounds = compat::rect_to_logical::(bounds, scale); + let bounds = bounds.to_logical::(scale); let bounds = cef::Rect { x: bounds.position.x, y: bounds.position.y, @@ -394,9 +401,18 @@ impl WinitCefApp { height: bounds.size.height, }; - // Published PendingWebview has no per-webview platform attribute channel - // (feat/cef-only), so the runtime style is always CEF's default. - let cef_runtime_style = cef::RuntimeStyle::DEFAULT; + // Let CEF pick the runtime style unless overridden per-webview. + let cef_runtime_style = pending + .platform_specific_attributes + .iter() + .map(|attr| match attr { + WebviewAtribute::RuntimeStyle { style } => match style { + RuntimeStyle::Alloy => cef::RuntimeStyle::ALLOY, + RuntimeStyle::Chrome => cef::RuntimeStyle::CHROME, + }, + }) + .next() + .unwrap_or(cef::RuntimeStyle::DEFAULT); let mut window_info = cef::WindowInfo::default().set_as_child(parent, &bounds); window_info.runtime_style = cef_runtime_style; @@ -586,19 +602,7 @@ impl WinitCefApp { WebviewMessage::CanGoBack(tx) => _ = tx.send(Ok(child.browser.can_go_back() == 1)), WebviewMessage::GoForward => child.browser.go_forward(), WebviewMessage::CanGoForward(tx) => _ = tx.send(Ok(child.browser.can_go_forward() == 1)), - // Tauri's Webview::close() is an unconditional native lifecycle action, - // not a page-requested window.close(). A non-forced CEF close may leave - // the child browser (and publisher code) alive indefinitely, and its late - // callback can race parent-window bookkeeping. Window/app teardown already - // uses force_close=true; standalone child close needs the same semantics. - WebviewMessage::Close => { - child.host.close_browser(1); - // Windowed CEF browsers are not destroyed by CloseBrowser alone: the - // native child hierarchy must also be torn down before OnBeforeClose - // runs. Leaving it attached leaks the renderer; letting CEF forward a - // close to its top-level parent can close the whole Tauri window. - child.destroy_native(); - } + WebviewMessage::Close => child.host.close_browser(0), WebviewMessage::SetBounds(bounds) => { let parent_size = appwindow.window.surface_size(); let scale = appwindow.window.scale_factor(); @@ -751,6 +755,11 @@ impl WinitCefApp { target_appwindow.window.scale_factor(), bounds, ); + // Re-parenting does not preserve z-order: a view docked back into a + // window that already owns a full-window main webview must be put back + // on top, or it lands behind it and renders nothing. + #[cfg(windows)] + child.raise_to_top(); target_appwindow.children.push(child); let _ = tx.send(Ok(())); @@ -800,6 +809,20 @@ impl WinitCefApp { } } +#[derive(Clone, Copy, Debug)] +pub enum RuntimeStyle { + Alloy, + Chrome, +} + +#[derive(Debug)] +pub enum WebviewAtribute { + RuntimeStyle { style: RuntimeStyle }, +} + +unsafe impl Send for WebviewAtribute {} +unsafe impl Sync for WebviewAtribute {} + #[derive(Debug, Clone)] pub struct CefInitScript { pub(crate) script: String, @@ -930,34 +953,6 @@ macro_rules! webview_getter { }}; } -impl CefWebviewDispatcher { - // History navigation: feat/cef trait methods, not yet part of the published - // `WebviewDispatch` trait — kept as inherent API until upstream releases. - pub fn go_back(&self) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::GoBack, - }) - } - - pub fn can_go_back(&self) -> Result { - webview_getter!(self, CanGoBack) - } - - pub fn go_forward(&self) -> Result<()> { - self.context.send_message(Message::Webview { - window_id: *self.window_id.lock().unwrap(), - webview_id: self.webview_id, - message: WebviewMessage::GoForward, - }) - } - - pub fn can_go_forward(&self) -> Result { - webview_getter!(self, CanGoForward) - } -} - impl WebviewDispatch for CefWebviewDispatcher { type Runtime = CefRuntime; @@ -975,13 +970,14 @@ impl WebviewDispatch for CefWebviewDispatcher { id } - fn with_webview) + Send + 'static>(&self, f: F) -> Result<()> { - // Published tauri erases the runtime webview type; downcast the boxed - // `Any` back to [`Webview`] to reach the underlying `cef::Browser`. + fn with_webview>::Webview) + Send + 'static>( + &self, + f: F, + ) -> Result<()> { self.context.send_message(Message::Webview { window_id: *self.window_id.lock().unwrap(), webview_id: self.webview_id, - message: WebviewMessage::WithWebview(Box::new(move |webview: Webview| f(Box::new(webview)))), + message: WebviewMessage::WithWebview(Box::new(f)), }) } @@ -1046,6 +1042,30 @@ impl WebviewDispatch for CefWebviewDispatcher { }) } + fn go_back(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::GoBack, + }) + } + + fn can_go_back(&self) -> Result { + webview_getter!(self, CanGoBack) + } + + fn go_forward(&self) -> Result<()> { + self.context.send_message(Message::Webview { + window_id: *self.window_id.lock().unwrap(), + webview_id: self.webview_id, + message: WebviewMessage::GoForward, + }) + } + + fn can_go_forward(&self) -> Result { + webview_getter!(self, CanGoForward) + } + fn print(&self) -> Result<()> { self.context.send_message(Message::Webview { window_id: *self.window_id.lock().unwrap(), diff --git a/src/window.rs b/src/window.rs index 15e7689..71c99e2 100644 --- a/src/window.rs +++ b/src/window.rs @@ -537,9 +537,7 @@ impl WinitCefApp { WindowMessage::AddEventListener(id, listener) => { appwindow.listeners.lock().unwrap().insert(id, listener); } - WindowMessage::Close | WindowMessage::Destroy => { - unreachable!("handled before borrowing") - } + WindowMessage::Close | WindowMessage::Destroy => unreachable!("handled before borrowing"), WindowMessage::ScaleFactor(tx) => _ = tx.send(Ok(window.scale_factor())), WindowMessage::InnerSize(tx) => _ = tx.send(Ok(window.surface_size())), WindowMessage::OuterSize(tx) => _ = tx.send(Ok(window.outer_size())), @@ -1208,7 +1206,7 @@ impl WindowDispatch for CefWindowDispatcher { fn set_icon(&self, icon: Icon) -> Result<()> { self.context.send_message(Message::Window { window_id: self.window_id, - message: WindowMessage::SetIcon(crate::compat::icon_into_owned(icon)), + message: WindowMessage::SetIcon(icon.into_owned()), }) } @@ -1285,7 +1283,7 @@ impl WindowDispatch for CefWindowDispatcher { fn set_overlay_icon(&self, icon: Option) -> Result<()> { self.context.send_message(Message::Window { window_id: self.window_id, - message: WindowMessage::SetOverlayIcon(icon.map(crate::compat::icon_into_owned)), + message: WindowMessage::SetOverlayIcon(icon.map(Icon::into_owned)), }) } @@ -1336,16 +1334,17 @@ where { let label = pending.label.clone(); let window_id = context.next_window_id(); - let (webview_id, use_https_scheme) = pending + let (webview_id, use_https_scheme, devtools) = pending .webview .as_ref() .map(|w| { ( Some(context.next_webview_id()), w.webview_attributes.use_https_scheme, + w.webview_attributes.devtools, ) }) - .unwrap_or((None, false)); + .unwrap_or((None, false, None)); let (result_tx, result_rx) = mpsc::channel(); context.send_message(Message::CreateWindow { @@ -1371,6 +1370,7 @@ where }, }, use_https_scheme, + devtools, }); Ok(DetachedWindow { diff --git a/src/window_builder.rs b/src/window_builder.rs index 9164cf0..7d5bf30 100644 --- a/src/window_builder.rs +++ b/src/window_builder.rs @@ -341,6 +341,9 @@ impl WindowBuilder for WindowBuilderWrapper { self.attrs.skip_taskbar = skip; } + #[cfg(target_os = "macos")] + let _skip = skip; + self } @@ -407,12 +410,28 @@ impl WindowBuilder for WindowBuilderWrapper { #[cfg(target_os = "macos")] fn parent(mut self, parent: *mut std::ffi::c_void) -> Self { - if let Some(ns_view) = NonNull::new(parent) { - let handle = - RawWindowHandle::AppKit(winit::raw_window_handle::AppKitWindowHandle::new(ns_view)); - // SAFETY: Tauri passes a live parent NSView owned by the application. - self.attrs.inner = unsafe { self.attrs.inner.with_parent_window(Some(handle)) }; - } + use objc2::rc::Retained; + use objc2_app_kit::{NSView, NSWindow}; + + let Some(nswindow) = NonNull::new(parent) else { + return self; + }; + let Some(nswindow) = (unsafe { Retained::::from_raw(nswindow.as_ptr() as _) }) else { + return self; + }; + + let Some(nsview) = nswindow.contentView() else { + return self; + }; + let nsview = Retained::::into_raw(nsview); + let Some(nsview) = NonNull::new(nsview as _) else { + return self; + }; + + let handle = winit::raw_window_handle::AppKitWindowHandle::new(nsview); + let handle = RawWindowHandle::AppKit(handle); + self.attrs.inner = unsafe { self.attrs.inner.with_parent_window(Some(handle)) }; + self } @@ -498,6 +517,11 @@ impl WindowBuilder for WindowBuilderWrapper { self } + // TODO + fn no_redirection_bitmap(#[allow(unused_mut)] mut self, _enable: bool) -> Self { + self + } + fn has_icon(&self) -> bool { self.attrs.inner.window_icon.is_some() }