//! CEF itself: the framework, its processes, its message pump. use std::{ path::{Path, PathBuf}, sync::{Mutex, OnceLock}, time::Duration, }; use anyhow::Result; use cef::{args::Args, *}; use futures::{FutureExt as _, StreamExt as _, channel::mpsc}; use gpui::{App, AsyncApp, Global, WeakEntity}; use crate::{handlers, surface::Shared, view::WebView}; /// The framework beside the app, else the fetched one. #[cfg(target_os = "macos")] fn framework_dir() -> Result { use anyhow::Context as _; const NAME: &str = "Chromium Embedded Framework.framework"; let exe = std::env::current_exe()?; let mut places: Vec = Vec::new(); if let Some(dir) = std::env::var_os("ELY_CEF") { places.push(PathBuf::from(dir).join(NAME)); } if let Some(contents) = exe.parent().and_then(Path::parent) { places.push(contents.join("Frameworks").join(NAME)); // A helper sits in Frameworks/ Helper.app/Contents/MacOS. if let Some(frameworks) = contents.parent().and_then(Path::parent) { places.push(frameworks.join(NAME)); } } if let Some(built) = option_env!("CEF_PATH") { places.push(Path::new(built).join(NAME)); } places .into_iter() .find(|place| place.is_dir()) .with_context(|| format!("no {NAME} beside {} or in engines/cef", exe.display())) } static LOADED: OnceLock> = OnceLock::new(); /// Loads CEF once and sets its API version. fn load() -> Result { LOADED .get_or_init(|| { let dir = open()?; // Versioned calls exit the process until this runs. let _ = api_hash(sys::CEF_API_VERSION_LAST, 0); log::info!("web: CEF loaded from {}", dir.display()); Ok(dir) }) .clone() .map_err(anyhow::Error::msg) } /// Loads the framework; its folder. #[cfg(target_os = "macos")] fn open() -> std::result::Result { let dir = framework_dir().map_err(|error| format!("{error:#}"))?; let library = dir.join("Chromium Embedded Framework"); let name = std::ffi::CString::new(library.as_os_str().as_encoded_bytes()).map_err(|error| error.to_string())?; if unsafe { load_library(Some(&*name.as_ptr().cast())) } != 1 { return Err(format!("CEF did not load from {}", library.display())); } Ok(dir) } /// The folder of the libcef.dll this process linked. #[cfg(windows)] fn open() -> std::result::Result { use std::os::windows::ffi::OsStringExt as _; use windows_sys::Win32::System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW}; let name: Vec = "libcef.dll".encode_utf16().chain([0]).collect(); let module = unsafe { GetModuleHandleW(name.as_ptr()) }; if module.is_null() { return Err(format!("libcef.dll is not loaded: {}", std::io::Error::last_os_error())); } let mut path = vec![0u16; 32_768]; let len = unsafe { GetModuleFileNameW(module, path.as_mut_ptr(), path.len() as u32) } as usize; if len == 0 || len == path.len() { return Err(format!("libcef.dll has no path: {}", std::io::Error::last_os_error())); } let file = PathBuf::from(std::ffi::OsString::from_wide(&path[..len])); file.parent().map(Path::to_path_buf).ok_or_else(|| format!("{} has no folder", file.display())) } /// The folder of the libcef.so this process linked. #[cfg(target_os = "linux")] fn open() -> std::result::Result { use std::os::unix::ffi::OsStrExt as _; let mut info: libc::Dl_info = unsafe { std::mem::zeroed() }; let entry = sys::cef_api_hash as *const libc::c_void; if unsafe { libc::dladdr(entry, &mut info) } == 0 || info.dli_fname.is_null() { return Err("libcef.so is not loaded".into()); } let name = unsafe { std::ffi::CStr::from_ptr(info.dli_fname) }; let file = PathBuf::from(std::ffi::OsStr::from_bytes(name.to_bytes())); file.parent().map(Path::to_path_buf).ok_or_else(|| format!("{} has no folder", file.display())) } /// In a CEF helper process, runs it to its end. pub fn subprocess() -> Option { // CEF's own parser needs the framework; plain args do not. if !std::env::args().any(|arg| arg.starts_with("--type=")) { return None; } if let Err(error) = load() { eprintln!("web: {error:#}"); return Some(1); } let args = Args::new(); let mut app = handlers::app(); let code = execute_process( Some(args.as_main_args()), Some(&mut app), std::ptr::null_mut(), ); Some(code) } /// The longest the pump waits between turns, as in cefclient. const FALLBACK: Duration = Duration::from_millis(1000 / 30); /// Pump work CEF asked for, from any thread. static WAKE: Mutex>> = Mutex::new(None); /// CEF wants its loop run within `delay_ms`. pub(crate) fn schedule(delay_ms: i64) { if let Some(wake) = WAKE.lock().expect("the pump lock").as_ref() { let _ = wake.unbounded_send(delay_ms); } } /// The live views the pump hands frames to. #[derive(Default)] pub(crate) struct Engine { pub views: Vec>, } impl Global for Engine {} thread_local! { /// CEF calls asked inside GPUI's updates, run after them. static LATER: std::cell::RefCell>> = Default::default(); } /// Runs `work` on the pump's next turn. pub(crate) fn later(work: impl FnOnce() + 'static) { LATER.with_borrow_mut(|queue| queue.push(Box::new(work))); schedule(0); } /// In asked order; work may ask for more. fn run_later() { loop { let batch = LATER.with_borrow_mut(std::mem::take); if batch.is_empty() { return; } for work in batch { work(); } } } /// Browsers asked for, made on the pump's next turn. static PENDING: Mutex> = Mutex::new(Vec::new()); /// Why CEF could not start; every view is told. static FAILED: Mutex> = Mutex::new(None); pub(crate) fn failed() -> Option { FAILED.lock().expect("the start lock").clone() } /// Queues a browser for `surface` at `url`. pub(crate) fn ask_browser(surface: Shared, url: String) { PENDING .lock() .expect("the pending lock") .push((surface, url)); schedule(0); } /// Outside GPUI's updates: CEF may spin the run loop. fn make_browsers() { let pending = std::mem::take(&mut *PENDING.lock().expect("the pending lock")); for (surface, url) in pending { if surface.lock().expect("the surface lock").closed { continue; } // Off-screen pages work only in CEF's Alloy style. let info = WindowInfo { windowless_rendering_enabled: true as _, runtime_style: RuntimeStyle::ALLOY, ..Default::default() }; let settings = BrowserSettings { windowless_frame_rate: 60, ..Default::default() }; // Made at once: no view ever waits on a browser. let made = browser_host_create_browser_sync( Some(&info), Some(&mut handlers::client(&surface)), Some(&url.as_str().into()), Some(&settings), None, None, ); match made { Some(browser) => { handlers::adopt(&browser, surface.clone()); surface.lock().expect("the surface lock").browser = Some(browser); } None => { surface.lock().expect("the surface lock").failed = Some(format!("CEF made no browser for {url}")) } } } } /// Starts CEF in this process; once per app. pub fn start(data: &Path, cx: &mut App) -> Result<()> { if cx.has_global::() { return Ok(()); } crate::input::bind(cx); let framework = load()?; #[cfg(target_os = "macos")] crate::mac::adopt_app_protocol()?; let exe = std::env::current_exe()?; // Bundled, CEF finds its helper apps; else this binary helps. let bundled = cfg!(target_os = "macos") && framework.parent().is_some_and(|dir| { let name = exe.file_name().unwrap_or_default().to_string_lossy(); dir.join(format!("{name} Helper.app")).is_dir() }); let helper = if bundled { String::new() } else { exe.to_string_lossy().into_owned() }; let cache = data.join("web"); std::fs::create_dir_all(&cache)?; let settings = Settings { windowless_rendering_enabled: true as _, external_message_pump: true as _, no_sandbox: true as _, browser_subprocess_path: helper.as_str().into(), #[cfg(target_os = "macos")] framework_dir_path: framework.to_string_lossy().as_ref().into(), #[cfg(not(target_os = "macos"))] resources_dir_path: framework.to_string_lossy().as_ref().into(), #[cfg(not(target_os = "macos"))] locales_dir_path: framework.join("locales").to_string_lossy().as_ref().into(), root_cache_path: cache.to_string_lossy().as_ref().into(), persist_session_cookies: false as _, log_severity: LogSeverity::WARNING, ..Default::default() }; let (wake, mut asked) = mpsc::unbounded(); *WAKE.lock().expect("the pump lock") = Some(wake); cx.set_global(Engine::default()); cx.spawn(async move |cx: &mut AsyncApp| { // Not inside an update: CEF may run AppKit's loop here. let args = Args::new(); let mut app = handlers::app(); let started = initialize( Some(args.as_main_args()), Some(&settings), Some(&mut app), std::ptr::null_mut(), ); if started != 1 { let why = held(&cache).unwrap_or_else(|| "CEF did not initialize".into()); log::error!("web: {why}"); *FAILED.lock().expect("the start lock") = Some(why); cx.update(|cx| crate::view::deliver(cx)); return; } log::info!("web: CEF started, cache {}", cache.display()); let quitting = cx.update(|cx| cx.on_app_quit(|_| async { stop() })); quitting.detach(); let mut due: Option = Some(Duration::ZERO); loop { let timer = match due { Some(delay) => cx.background_executor().timer(delay).boxed_local(), None => futures::future::pending().boxed_local(), }; futures::select_biased! { delay = asked.next() => match delay { Some(delay) => { let delay = Duration::from_millis(delay.max(0) as u64); due = Some(due.map_or(delay, |now| now.min(delay))); continue; } None => return, }, _ = timer.fuse() => {} } run_later(); make_browsers(); do_message_loop_work(); cx.update(|cx| crate::view::deliver(cx)); // As cefclient: CEF does not ask for all work. due = Some(FALLBACK); } }) .detach(); Ok(()) } /// Stops CEF as the app quits. fn stop() { WAKE.lock().expect("the pump lock").take(); shutdown(); log::info!("web: CEF stopped"); } /// Why CEF would refuse this cache: another app holds it. #[cfg(unix)] fn held(cache: &Path) -> Option { let lock = std::fs::read_link(cache.join("SingletonLock")).ok()?; let lock = lock.to_string_lossy(); let pid: i32 = lock.rsplit('-').next()?.parse().ok()?; // Only a live process holds it. let alive = unsafe { libc::kill(pid, 0) } == 0; alive.then(|| format!("another copy (process {pid}) is using {}", cache.display())) } /// Chromium's Windows lock names no process. #[cfg(windows)] fn held(_cache: &Path) -> Option { None } #[cfg(test)] mod tests { #[test] fn cef_loads_with_its_api_version() { let dir = super::load().expect("CEF loads"); #[cfg(not(target_os = "macos"))] assert!(dir.join("locales").is_dir(), "{} holds CEF's files", dir.display()); // Without the version this call exits the process. assert!(cef::command_line_create().is_some()); } }