Files
ElyOffice/crates/ely-web-content/src/engine.rs
T
ZacharyZhang-NYandClaude Opus 5.5 0d4dd64640 Web pages: browsers are made at once, so none outlives its view (M-12)
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>
2026-10-10 13:47:45 -04:00

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());
}
}