make_browsers creates each browser synchronously and adopts it in the same call, so no view waits on a browser: it leaves before creation (skipped) or has a browser to close. The late-close branch in on_after_created is gone. webprobe drops 5 views before the pump and 10 within 27 ms; webview.sh wants all 35 freed before the hold. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
352 lines
12 KiB
Rust
352 lines
12 KiB
Rust
//! 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<PathBuf> {
|
|
use anyhow::Context as _;
|
|
const NAME: &str = "Chromium Embedded Framework.framework";
|
|
let exe = std::env::current_exe()?;
|
|
let mut places: Vec<PathBuf> = 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/<App> 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<std::result::Result<PathBuf, String>> = OnceLock::new();
|
|
|
|
/// Loads CEF once and sets its API version.
|
|
fn load() -> Result<PathBuf> {
|
|
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<PathBuf, String> {
|
|
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<PathBuf, String> {
|
|
use std::os::windows::ffi::OsStringExt as _;
|
|
use windows_sys::Win32::System::LibraryLoader::{GetModuleFileNameW, GetModuleHandleW};
|
|
let name: Vec<u16> = "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<PathBuf, String> {
|
|
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<i32> {
|
|
// 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<Option<mpsc::UnboundedSender<i64>>> = 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<WeakEntity<WebView>>,
|
|
}
|
|
|
|
impl Global for Engine {}
|
|
|
|
thread_local! {
|
|
/// CEF calls asked inside GPUI's updates, run after them.
|
|
static LATER: std::cell::RefCell<Vec<Box<dyn FnOnce()>>> = 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<Vec<(Shared, String)>> = Mutex::new(Vec::new());
|
|
|
|
/// Why CEF could not start; every view is told.
|
|
static FAILED: Mutex<Option<String>> = Mutex::new(None);
|
|
|
|
pub(crate) fn failed() -> Option<String> {
|
|
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::<Engine>() {
|
|
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<Duration> = 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<String> {
|
|
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<String> {
|
|
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());
|
|
}
|
|
}
|