diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ede577a5b..faabdfdaf 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -785,9 +785,9 @@ dependencies = [ [[package]] name = "cef" -version = "148.0.0+147.0.10" +version = "150.2.1+150.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2445d2f1e820efc064c511faa3a07c3ee79e565fdae026ca62ed3c80d0459223" +checksum = "b912d9d13dd63b4bffa750f1f3c49271dc96d6c91ca2d1c1f68a0172832170c8" dependencies = [ "anyhow", "cargo_metadata 0.23.1", @@ -805,9 +805,9 @@ dependencies = [ [[package]] name = "cef-dll-sys" -version = "148.0.0+147.0.10" +version = "150.2.1+150.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d32a7c89af6c77688ad58dc75555c9398c5ded2c328a3ff83554fccc840af57" +checksum = "31b1059b49a0d5aa3872035f076210a31df6d70e039e12427b41ceba7c650b49" dependencies = [ "anyhow", "cmake", @@ -1585,7 +1585,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ - "libloading 0.8.9", + "libloading 0.7.4", ] [[package]] @@ -2822,7 +2822,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.5", "system-configuration", "tokio", "tower-service", @@ -4798,7 +4798,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.5.10", + "socket2 0.6.5", "thiserror 2.0.19", "tokio", "tracing", @@ -4836,7 +4836,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.5", "tracing", "windows-sys 0.60.2", ] @@ -6913,7 +6913,7 @@ dependencies = [ [[package]] name = "tauri-runtime-cef" version = "0.1.0" -source = "git+https://github.com/byeongsu-hong/tauri-runtime-cef?rev=c215d6e52dd507956d9b8cc868906d690dc477c2#c215d6e52dd507956d9b8cc868906d690dc477c2" +source = "git+https://github.com/SableClient/tauri-runtime-cef?branch=main#23464067ddcfe61239e41faeb26e1ce200ff3242" dependencies = [ "base64 0.22.1", "cef", @@ -6926,6 +6926,7 @@ dependencies = [ "html5ever 0.29.1", "http 1.4.2", "kuchikiki", + "libc", "log", "objc2", "objc2-app-kit", @@ -7025,7 +7026,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.19", - "toml 1.1.3+spec-1.1.0", + "toml 0.9.12+spec-1.1.0", "url", "urlpattern", "uuid", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 07b7be258..08b96e3cd 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -109,11 +109,11 @@ gtk = "0.18" webkit2gtk = { version = "2.0", optional = true } # CEF (Chromium) runtime — Linux only; other platforms use wry. -# Repackage of tauri's unreleased feat/cef branch. Switch to the official -# tauri-runtime-cef once it lands on crates.io. +# Fork of tauri's unreleased feat/cef branch, ported onto published crates. +# https://github.com/SableClient/tauri-runtime-cef [target.'cfg(target_os = "linux")'.dependencies] -tauri-runtime-cef = { git = "https://github.com/byeongsu-hong/tauri-runtime-cef", rev = "c215d6e52dd507956d9b8cc868906d690dc477c2", optional = true } -cef = { version = "=148.0.0", optional = true } +tauri-runtime-cef = { git = "https://github.com/SableClient/tauri-runtime-cef", branch = "main", optional = true } +cef = { version = "=150.2.1", optional = true } libloading = "0.8" [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 997903109..82200209c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,38 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +#[cfg(all(feature = "cef", target_os = "linux"))] +fn prompt_cef_permission(message: String, tx: std::sync::mpsc::Sender) { + use gtk::glib; + use gtk::prelude::*; + use gtk::{ButtonsType, DialogFlags, MessageDialog, MessageType, ResponseType}; + + // Non-blocking: show() + response signal instead of run(). + // dialog.run() blocks the GLib main loop, which CEF's message pump + // also uses on Linux, freezing the app. + glib::idle_add_once(move || { + let dialog = MessageDialog::new( + None::<>k::Window>, + DialogFlags::MODAL, + MessageType::Question, + ButtonsType::YesNo, + &message, + ); + dialog.set_title("Permission request"); + + let tx = std::cell::RefCell::new(Some(tx)); + let dialog_ptr = dialog.clone(); + dialog.connect_response(move |dlg, response| { + if let Some(tx) = tx.take() { + let _ = tx.send(matches!(response, ResponseType::Yes)); + } + dlg.close(); + }); + + dialog.show(); + }); +} + fn main() { // CEF (Chromium) runtime, Linux only. Must run before anything else — CEF // re-execs this binary for its subprocesses. @@ -20,15 +52,23 @@ fn main() { ("--disable-gpu-sandbox".into(), None), ("--disable-font-subpixel-positioning".into(), None), ("--enable-font-antialiasing".into(), None), - ("autoplay-policy".into(), Some("no-user-gesture-required".into())), - ("enable-features".into(), Some("SharedArrayBuffer".into())), + ( + "autoplay-policy".into(), + Some("no-user-gesture-required".into()), + ), + ( + "enable-features".into(), + Some("SharedArrayBuffer".into()), + ), ("--disable-background-timer-throttling".into(), None), - ("--disable-renderer-backgrounding".into(), None), - ("--disable-backgrounding-occluded-windows".into(), None), - ("disable-features".into(), Some( - "SpareRendererForSitePerProcess,IntensiveWakeUpThrottling,CalculateNativeWinOcclusion,AutofillActorMode,GlicActorUi,LensOverlay,LocalNetworkAccessChecks,LocalNetworkAccessChecksWebSocket,LocalNetworkAccessChecksWebRTC".into() - )), + ("--skia-resource-cache-limit-mb".into(), Some("64".into())), + ("--renderer-process-limit".into(), Some("2".into())), + ( + "disable-features".into(), + Some("SpareRendererForSitePerProcess,IntensiveWakeUpThrottling,AutofillActorMode,GlicActorUi,LensOverlay,LocalNetworkAccessChecks,LocalNetworkAccessChecksWebSocket,LocalNetworkAccessChecksWebRTC".into()), + ), ]; + // Remote debugging via SABLE_DEVTOOLS=port env var. // Open chrome://inspect in Chrome to connect. if let Ok(port) = std::env::var("SABLE_DEVTOOLS") { @@ -54,24 +94,94 @@ fn main() { } // Allow call media capture (mic, camera, screen-share) for our webview. - tauri_runtime_cef::set_permission_policy(|request, responder| { - use tauri_runtime_cef::{DenyReason, PermissionKind, Verdict}; - if request.webview_label == "main" { - let verdicts = request - .kinds - .iter() - .map(|kind| match kind { + // Cache granted permissions so we only prompt once per kind. + use std::collections::HashSet; + use std::sync::{Mutex, OnceLock}; + static GRANTED: OnceLock>> = OnceLock::new(); + let granted = GRANTED.get_or_init(|| Mutex::new(HashSet::new())); + + tauri_runtime_cef::set_permission_policy(move |request, responder| { + use tauri_runtime_cef::{DenyReason, PermissionKind}; + + if request.webview_label != "main" { + return responder.deny(DenyReason::NoPolicy); + } + + let media_kinds: Vec = request + .kinds + .iter() + .filter(|kind| { + matches!( + kind, PermissionKind::Microphone - | PermissionKind::Camera - | PermissionKind::CameraPanTiltZoom - | PermissionKind::ScreenCapture - | PermissionKind::CapturedSurfaceControl => Verdict::Allow, - _ => Verdict::Deny, - }) - .collect(); - return responder.decide(verdicts); + | PermissionKind::Camera + | PermissionKind::CameraPanTiltZoom + | PermissionKind::ScreenCapture + | PermissionKind::CapturedSurfaceControl + ) + }) + .cloned() + .collect(); + + if media_kinds.is_empty() { + return responder.deny(DenyReason::NoPolicy); + } + + // Check cache: if all requested kinds were already granted, allow immediately. + let kind_names: Vec<&'static str> = media_kinds + .iter() + .map(|k| match k { + PermissionKind::Microphone => "mic", + PermissionKind::Camera | PermissionKind::CameraPanTiltZoom => "cam", + PermissionKind::ScreenCapture | PermissionKind::CapturedSurfaceControl => { + "screen" + } + _ => "other", + }) + .collect(); + + { + let cache = granted.lock().unwrap(); + if kind_names.iter().all(|k| cache.contains(k)) { + return responder.allow(); + } } - responder.deny(DenyReason::NoPolicy) + + let msg = match media_kinds.as_slice() { + [PermissionKind::Microphone] => "Sable wants to access your microphone.", + [PermissionKind::Camera] => "Sable wants to access your camera.", + [PermissionKind::ScreenCapture] | [PermissionKind::CapturedSurfaceControl] => { + "Sable wants to share your screen." + } + _ if media_kinds.contains(&PermissionKind::Microphone) + && media_kinds.contains(&PermissionKind::Camera) => + { + "Sable wants to access your microphone and camera." + } + _ => "Sable wants to access media devices.", + }; + + // Defer the CEF callback and ask the user via a native GTK dialog. + // The policy runs on a CEF thread and must not block; defer() hands + // the answer to a worker thread that waits on the dialog result. + let deferred = responder.defer(tauri_runtime_cef::DEFAULT_PROMPT_TIMEOUT); + let (tx, rx) = std::sync::mpsc::channel(); + let kinds_to_cache = kind_names.clone(); + prompt_cef_permission(msg.to_string(), tx); + std::thread::spawn(move || { + let allowed = rx + .recv_timeout(tauri_runtime_cef::DEFAULT_PROMPT_TIMEOUT) + .unwrap_or(false); + if allowed { + let mut cache = granted.lock().unwrap(); + for k in &kinds_to_cache { + cache.insert(k); + } + deferred.allow(); + } else { + deferred.deny(DenyReason::PolicyDenied); + } + }); }); }