use crate::RunOnMainThread;
use crate::common::WebviewCoreComponentsPlugin;
use crate::common::ipc::js_emit::IpcRawEventPlugin;
use crate::focus::FocusedWebview;
use crate::keyboard::CefKeyboardFilter;
use crate::prelude::{
BeginFrameInterval, WebviewExtendStandardMaterial, WebviewMaterial, WebviewUiMaterial,
};
use bevy::ecs::system::ScheduleSystem;
use bevy::prelude::*;
use bevy_cef_core::prelude::*;
use cef::args::Args;
use cef::{Settings, api_hash, execute_process, initialize, shutdown, sys};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub enum CefStartupError {
RootCacheDir {
path: String,
error: String,
},
LibraryLoad,
Initialize {
exit_code: i32,
},
}
impl CefStartupError {
pub fn is_root_in_use(&self) -> bool {
match self {
Self::Initialize { exit_code } => {
*exit_code == sys::cef_resultcode_t::CEF_RESULT_CODE_PROFILE_IN_USE as i32
|| *exit_code
== sys::cef_resultcode_t::CEF_RESULT_CODE_NORMAL_EXIT_PROCESS_NOTIFIED
as i32
}
_ => false,
}
}
}
impl std::fmt::Display for CefStartupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::RootCacheDir { path, error } => {
write!(
f,
"failed to create root_cache_path directory '{path}': {error}"
)
}
Self::LibraryLoad => write!(f, "failed to load the CEF library"),
Self::Initialize { exit_code } => {
write!(f, "cef_initialize failed with exit code {exit_code}")
}
}
}
}
impl std::error::Error for CefStartupError {}
#[derive(Resource, Debug, Clone)]
pub enum CefStatus {
Running,
Failed(CefStartupError),
}
#[derive(Resource)]
pub(crate) struct CefRootCachePath(pub(crate) Option<String>);
#[derive(Resource)]
pub(crate) struct CefShutDown;
pub struct MessageLoopPlugin {
pub config: CommandLineConfig,
pub extensions: CefExtensions,
pub root_cache_path: Option<String>,
pub no_sandbox: bool,
}
impl MessageLoopPlugin {
pub(crate) fn start(&self, app: &mut App) -> Result<(), CefStartupError> {
#[cfg(not(target_os = "macos"))]
let render_process_binary = render_process_path();
#[cfg(target_os = "macos")]
load_cef_library(app)?;
let _ = api_hash(sys::CEF_API_VERSION_LAST, 0);
let args = Args::new();
let (tx, rx) = std::sync::mpsc::channel();
let mut cef_app =
BrowserProcessAppBuilder::build(tx, self.config.clone(), self.extensions.clone());
#[cfg(target_os = "macos")]
{
let ret = execute_process(
Some(args.as_main_args()),
Some(&mut cef_app),
std::ptr::null_mut(),
);
if ret >= 0 {
std::process::exit(ret);
}
}
#[cfg(not(target_os = "macos"))]
cef_initialize(
&args,
&mut cef_app,
self.root_cache_path.as_deref(),
render_process_binary.as_deref(),
self.no_sandbox,
)?;
#[cfg(target_os = "macos")]
cef_initialize(
&args,
&mut cef_app,
self.root_cache_path.as_deref(),
self.no_sandbox,
)?;
app.insert_non_send(cef_app);
app.insert_non_send(MessageLoopWorkingReceiver(rx));
Ok(())
}
}
impl Plugin for MessageLoopPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
Main,
cef_do_message_loop_work.run_if(not(resource_exists::<CefShutDown>)),
);
#[cfg(all(target_os = "macos", feature = "debug"))]
app.add_systems(
Main,
macos::observe_terminate_request.before(cef_do_message_loop_work),
);
app.insert_non_send(RunOnMainThread);
register_on_app_exit(
app,
shutdown_cef_on_exit.run_if(not(resource_exists::<CefShutDown>)),
);
}
}
pub(crate) fn register_on_app_exit<M>(
app: &mut App,
system: impl IntoScheduleConfigs<ScheduleSystem, M>,
) {
app.add_systems(
Main,
system
.after(Main::run_main)
.after(cef_do_message_loop_work)
.run_if(on_message::<AppExit>),
);
}
pub(crate) fn add_failed_startup(app: &mut App, error: CefStartupError, root: Option<String>) {
app.insert_resource(CefStatus::Failed(error))
.insert_resource(CefRootCachePath(root))
.add_plugins((WebviewCoreComponentsPlugin, IpcRawEventPlugin))
.add_message::<RenderTextureMessage>()
.init_resource::<FocusedWebview>()
.init_resource::<CefKeyboardFilter>()
.init_resource::<BeginFrameInterval>()
.insert_non_send(RunOnMainThread);
if app.world().contains_resource::<AssetServer>() {
app.init_asset::<WebviewMaterial>()
.init_asset::<WebviewExtendStandardMaterial>()
.init_asset::<WebviewUiMaterial>()
.init_asset::<CefResponse>();
}
}
#[cfg(target_os = "macos")]
fn load_cef_library(app: &mut App) -> Result<(), CefStartupError> {
macos::install_cef_app_protocol();
#[cfg(all(target_os = "macos", feature = "debug"))]
let loader = DebugLibraryLoader::new();
#[cfg(all(target_os = "macos", not(feature = "debug")))]
let loader = {
let exe = std::env::current_exe().map_err(|_| CefStartupError::LibraryLoad)?;
cef::library_loader::LibraryLoader::new(&exe, false)
};
if !loader.load() {
return Err(CefStartupError::LibraryLoad);
}
app.insert_non_send(loader);
Ok(())
}
fn ensure_root_cache_dir(root_cache_path: Option<&str>) -> Result<(), CefStartupError> {
if let Some(path) = root_cache_path.filter(|p| !p.trim().is_empty()) {
std::fs::create_dir_all(path).map_err(|e| CefStartupError::RootCacheDir {
path: path.to_string(),
error: e.to_string(),
})?;
}
Ok(())
}
fn initialize_result(initialized: i32) -> Result<(), CefStartupError> {
if initialized == 1 {
Ok(())
} else {
Err(CefStartupError::Initialize {
exit_code: cef::get_exit_code(),
})
}
}
#[cfg(target_os = "macos")]
fn cef_initialize(
args: &Args,
cef_app: &mut cef::App,
root_cache_path: Option<&str>,
no_sandbox: bool,
) -> Result<(), CefStartupError> {
ensure_root_cache_dir(root_cache_path)?;
let settings = Settings {
#[cfg(feature = "debug")]
framework_dir_path: debug_chromium_embedded_framework_dir_path()
.to_str()
.unwrap()
.into(),
#[cfg(feature = "debug")]
browser_subprocess_path: debug_render_process_path().to_str().unwrap().into(),
no_sandbox: no_sandbox as _,
root_cache_path: root_cache_path.unwrap_or_default().into(),
windowless_rendering_enabled: true as _,
external_message_pump: true as _,
disable_signal_handlers: true as _,
..Default::default()
};
initialize_result(initialize(
Some(args.as_main_args()),
Some(&settings),
Some(cef_app),
std::ptr::null_mut(),
))
}
#[cfg(not(target_os = "macos"))]
fn cef_initialize(
args: &Args,
cef_app: &mut cef::App,
root_cache_path: Option<&str>,
render_process_binary: Option<&std::path::Path>,
no_sandbox: bool,
) -> Result<(), CefStartupError> {
ensure_root_cache_dir(root_cache_path)?;
let subprocess_path: String = render_process_binary
.and_then(|p| p.to_str())
.unwrap_or_default()
.into();
let settings = Settings {
browser_subprocess_path: subprocess_path.as_str().into(),
no_sandbox: no_sandbox as _,
root_cache_path: root_cache_path.unwrap_or_default().into(),
windowless_rendering_enabled: true as _,
external_message_pump: true as _,
disable_signal_handlers: false as _,
..Default::default()
};
initialize_result(initialize(
Some(args.as_main_args()),
Some(&settings),
Some(cef_app),
std::ptr::null_mut(),
))
}
fn cef_do_message_loop_work(
receiver: NonSend<MessageLoopWorkingReceiver>,
mut timer: Local<Option<MessageLoopTimer>>,
mut max_delay_timer: Local<MessageLoopWorkingMaxDelayTimer>,
mut last_execution: Local<Option<std::time::Instant>>,
) {
while let Ok(t) = receiver.try_recv() {
timer.replace(t);
}
let should_execute =
timer.as_ref().map(|t| t.is_finished()).unwrap_or(false) || max_delay_timer.is_finished();
if should_execute {
const MIN_PUMP_INTERVAL: std::time::Duration = std::time::Duration::from_millis(4);
let now = std::time::Instant::now();
if let Some(last) = *last_execution
&& now.duration_since(last) < MIN_PUMP_INTERVAL
{
return;
}
*last_execution = Some(now);
cef::do_message_loop_work();
*max_delay_timer = MessageLoopWorkingMaxDelayTimer::default();
timer.take();
}
}
fn shutdown_cef_on_exit(mut browsers: NonSendMut<Browsers>, mut commands: Commands) {
const SHUTDOWN_TIME_LIMIT: Duration = Duration::from_secs(3);
const PUMP_INTERVAL: Duration = Duration::from_millis(5);
browsers.close_all();
let deadline = Instant::now() + SHUTDOWN_TIME_LIMIT;
while browsers.live_browser_count() > 0 {
if Instant::now() >= deadline {
warn!(
"bevy_cef: {} browser(s) still open after {SHUTDOWN_TIME_LIMIT:?}; shutting CEF down anyway",
browsers.live_browser_count()
);
break;
}
cef::do_message_loop_work();
std::thread::sleep(PUMP_INTERVAL);
}
browsers.release_all();
shutdown();
commands.insert_resource(CefShutDown);
}
#[allow(clippy::needless_doctest_main)]
#[cfg(not(target_os = "macos"))]
pub fn early_exit_if_subprocess() {
let _ = api_hash(sys::CEF_API_VERSION_LAST, 0);
let args = Args::new();
let mut app = RenderProcessAppBuilder::build();
let ret = execute_process(
Some(args.as_main_args()),
Some(&mut app),
std::ptr::null_mut(),
);
if ret >= 0 {
std::process::exit(ret);
}
}
#[cfg(target_os = "macos")]
mod macos {
use core::sync::atomic::AtomicBool;
use objc::runtime::{Class, Object, Sel};
use objc::{sel, sel_impl};
use std::os::raw::c_char;
use std::os::raw::c_void;
use std::sync::atomic::Ordering;
unsafe extern "C" {
fn class_addMethod(
cls: *const Class,
name: Sel,
imp: *const c_void,
types: *const c_char,
) -> bool;
}
#[cfg(feature = "debug")]
use bevy::prelude::{AppExit, MessageWriter, info};
#[cfg(feature = "debug")]
use objc::runtime::Method;
#[cfg(feature = "debug")]
unsafe extern "C" {
fn class_getInstanceMethod(cls: *const Class, sel: Sel) -> *mut Method;
fn method_exchangeImplementations(m1: *mut Method, m2: *mut Method);
}
static IS_HANDLING_SEND_EVENT: AtomicBool = AtomicBool::new(false);
#[cfg(feature = "debug")]
static TERMINATE_REQUESTED: AtomicBool = AtomicBool::new(false);
extern "C" fn is_handling_send_event(_: &Object, _: Sel) -> bool {
IS_HANDLING_SEND_EVENT.load(Ordering::Relaxed)
}
extern "C" fn set_handling_send_event(_: &Object, _: Sel, flag: bool) {
IS_HANDLING_SEND_EVENT.swap(flag, Ordering::Relaxed);
}
#[cfg(feature = "debug")]
extern "C" fn swizzled_terminate(_: &Object, _: Sel, _sender: *mut Object) {
TERMINATE_REQUESTED.store(true, Ordering::Relaxed);
}
#[cfg(feature = "debug")]
fn install_terminate_swizzle() {
unsafe {
let cls = Class::get("NSApplication").expect("NSApplication class not found");
let placeholder_sel = sel!(cef_swizzled_terminate:);
let added = class_addMethod(
cls as *const _,
placeholder_sel,
swizzled_terminate as *const c_void,
c"v@:@".as_ptr() as *const c_char,
);
assert!(
added,
"Failed to add cef_swizzled_terminate: to NSApplication"
);
let terminate_method = class_getInstanceMethod(cls as *const _, sel!(terminate:));
assert!(
!terminate_method.is_null(),
"terminate: method not found on NSApplication"
);
let swizzled_method = class_getInstanceMethod(cls as *const _, placeholder_sel);
assert!(
!swizzled_method.is_null(),
"cef_swizzled_terminate: method not found after class_addMethod"
);
method_exchangeImplementations(terminate_method, swizzled_method);
}
}
pub fn install_cef_app_protocol() {
unsafe {
let cls = Class::get("NSApplication").expect("NSApplication class not found");
let sel_name = sel!(isHandlingSendEvent);
let success = class_addMethod(
cls as *const _,
sel_name,
is_handling_send_event as *const c_void,
c"c@:".as_ptr() as *const c_char,
);
assert!(
success,
"Failed to add isHandlingSendEvent to NSApplication"
);
let sel_set = sel!(setHandlingSendEvent:);
let success2 = class_addMethod(
cls as *const _,
sel_set,
set_handling_send_event as *const c_void,
c"v@:c".as_ptr() as *const c_char,
);
assert!(
success2,
"Failed to add setHandlingSendEvent: to NSApplication"
);
#[cfg(feature = "debug")]
install_terminate_swizzle();
}
}
#[cfg(feature = "debug")]
pub(super) fn observe_terminate_request(mut writer: MessageWriter<AppExit>) {
if TERMINATE_REQUESTED.swap(false, Ordering::Relaxed) {
info!("Termination intercepted, requesting AppExit");
writer.write(AppExit::from_code(130));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::JsEmitEventPlugin;
#[test]
fn root_in_use_covers_codes_21_and_24() {
assert!(CefStartupError::Initialize { exit_code: 21 }.is_root_in_use());
assert!(CefStartupError::Initialize { exit_code: 24 }.is_root_in_use());
assert!(!CefStartupError::Initialize { exit_code: 1 }.is_root_in_use());
assert!(!CefStartupError::LibraryLoad.is_root_in_use());
assert!(
!CefStartupError::RootCacheDir {
path: "p".into(),
error: "e".into(),
}
.is_root_in_use()
);
}
#[test]
fn a_failed_start_adds_no_cef_systems() {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
add_failed_startup(
&mut app,
CefStartupError::Initialize { exit_code: 21 },
Some("root".into()),
);
app.add_plugins(JsEmitEventPlugin::<serde_json::Value>::default());
assert!(matches!(
app.world().resource::<CefStatus>(),
CefStatus::Failed(_)
));
assert!(!app.world().contains_non_send::<Browsers>());
app.update();
}
#[test]
fn a_failed_start_keeps_example_startup_systems_running() {
fn setup(
_materials: ResMut<Assets<WebviewExtendStandardMaterial>>,
_ui_materials: ResMut<Assets<WebviewUiMaterial>>,
_filter: Res<CefKeyboardFilter>,
_interval: Res<BeginFrameInterval>,
_marker: NonSend<RunOnMainThread>,
) {
}
let mut app = App::new();
app.add_plugins((MinimalPlugins, AssetPlugin::default()));
add_failed_startup(
&mut app,
CefStartupError::Initialize { exit_code: 21 },
None,
);
app.add_systems(Startup, setup);
app.update();
}
#[test]
fn an_app_exit_written_in_last_reaches_the_exit_system() {
#[derive(Resource, Default)]
struct ExitSystemRan(bool);
fn mark_ran(mut ran: ResMut<ExitSystemRan>) {
ran.0 = true;
}
fn write_exit(mut exit: MessageWriter<AppExit>) {
exit.write(AppExit::Success);
}
let mut app = App::new();
app.init_resource::<ExitSystemRan>()
.add_systems(Last, write_exit);
register_on_app_exit(&mut app, mark_ran);
app.update();
assert!(app.world().resource::<ExitSystemRan>().0);
}
}