From e66a73c60e2192043b9fd70adfe5e8a53f6db0f9 Mon Sep 17 00:00:00 2001 From: Bill Avery Date: Sun, 19 Oct 2025 15:16:36 -0700 Subject: [PATCH] feat(test): port tests/shared library from CEF feat: unify bundle utilities under cef build-util feature feat: reenable sandbox support in cefsimple feat: add MainMenu.xib on mac fix: bypass tryToTerminateApplication for Command+Q fix: enable much smaller linux release builds --- .gitignore | 3 +- Cargo.toml | 9 +- README.md | 13 +- cef/Cargo.toml | 46 +- cef/src/args.rs | 13 +- cef/src/bin/bundle-cef-app/linux.rs | 26 + cef/src/bin/bundle-cef-app/mac.rs | 46 + cef/src/bin/bundle-cef-app/main.rs | 29 + cef/src/bin/bundle-cef-app/win.rs | 26 + cef/src/build_util/linux.rs | 77 + cef/src/build_util/mac.rs | 342 ++++ cef/src/build_util/metadata.rs | 86 + cef/src/build_util/mod.rs | 18 + .../src/build_util/win/cef-app.exe.manifest | 0 cef/src/build_util/win/mod.rs | 117 ++ cef/src/lib.rs | 11 + cef/src/sandbox.rs | 6 + cef/src/string.rs | 3 + cef/src/window_info.rs | 60 + cef/src/wrapper/browser_info_map.rs | 164 ++ cef/src/wrapper/byte_read_handler.rs | 110 + cef/src/wrapper/message_router.rs | 1773 +++++++++++++++++ cef/src/wrapper/message_router_utils.rs | 726 +++++++ cef/src/wrapper/mod.rs | 8 + cef/src/wrapper/resource_manager.rs | 1155 +++++++++++ cef/src/wrapper/stream_resource_handler.rs | 121 ++ cef/src/wrapper/zip_archive.rs | 131 ++ download-cef/src/lib.rs | 6 +- examples/cefsimple/Cargo.toml | 52 +- examples/cefsimple/build.rs | 14 + .../mac/English.lproj/InfoPlist.strings | 3 + .../resources/mac/English.lproj/MainMenu.xib | 330 +++ .../cefsimple/resources/mac/cefsimple.icns | Bin 0 -> 55080 bytes .../cefsimple/resources/win/cefsimple.ico | Bin 0 -> 23558 bytes examples/cefsimple/resources/win/small.ico | Bin 0 -> 23558 bytes .../helper.rs => bin/cefsimple_helper.rs} | 0 examples/cefsimple/src/lib.rs | 4 + .../cefsimple/src/mac/bundle_cefsimple.rs | 182 -- examples/cefsimple/src/mac/mod.rs | 243 +++ examples/cefsimple/src/main.rs | 243 +-- examples/cefsimple/src/shared/mod.rs | 75 + examples/cefsimple/src/shared/resources.rs | 2 + examples/cefsimple/src/shared/simple_app.rs | 215 ++ .../src/shared/simple_handler/linux.rs | 76 + .../src/shared/simple_handler/mac.rs | 29 + .../src/shared/simple_handler/mod.rs | 326 +++ .../src/shared/simple_handler/win.rs | 18 + examples/cefsimple/src/win.rs | 21 + examples/osr/Cargo.toml | 3 + examples/osr/src/webrender.rs | 14 +- examples/tests_shared/Cargo.toml | 34 + examples/tests_shared/resources/osr_test.html | 205 ++ examples/tests_shared/resources/pdf.html | 9 + examples/tests_shared/resources/pdf.pdf | Bin 0 -> 30110 bytes .../tests_shared/resources/window_icon.1x.png | Bin 0 -> 603 bytes .../tests_shared/resources/window_icon.2x.png | Bin 0 -> 212 bytes .../src/browser/client_app_browser.rs | 262 +++ .../tests_shared/src/browser/file_util.rs | 81 + .../tests_shared/src/browser/geometry_util.rs | 56 + .../src/browser/main_message_loop.rs | 135 ++ .../main_message_loop_external_pump/linux.rs | 243 +++ .../main_message_loop_external_pump/mac.rs | 135 ++ .../main_message_loop_external_pump/mod.rs | 218 ++ .../main_message_loop_external_pump/win.rs | 145 ++ .../src/browser/main_message_loop_std.rs | 46 + examples/tests_shared/src/browser/mod.rs | 10 + .../src/browser/resource_util/mod.rs | 9 + .../src/browser/resource_util/posix.rs | 42 + .../src/browser/resource_util/win.rs | 124 ++ examples/tests_shared/src/browser/util_win.rs | 256 +++ .../src/common/binary_value_utils.rs | 147 ++ .../tests_shared/src/common/client_app.rs | 66 + .../src/common/client_app_other.rs | 13 + .../src/common/client_switches.rs | 52 + examples/tests_shared/src/common/mod.rs | 4 + examples/tests_shared/src/lib.rs | 6 + .../tests_shared/src/process_helper_mac.rs | 44 + .../src/renderer/client_app_renderer.rs | 255 +++ examples/tests_shared/src/renderer/mod.rs | 1 + export-cef-dir/src/main.rs | 2 +- get-latest/src/main.rs | 2 +- update-bindings/src/main.rs | 2 +- update-bindings/src/parse_tree.rs | 1 + 83 files changed, 9135 insertions(+), 445 deletions(-) create mode 100644 cef/src/bin/bundle-cef-app/linux.rs create mode 100644 cef/src/bin/bundle-cef-app/mac.rs create mode 100644 cef/src/bin/bundle-cef-app/main.rs create mode 100644 cef/src/bin/bundle-cef-app/win.rs create mode 100644 cef/src/build_util/linux.rs create mode 100644 cef/src/build_util/mac.rs create mode 100644 cef/src/build_util/metadata.rs create mode 100644 cef/src/build_util/mod.rs rename examples/cefsimple/src/win/cefsimple.exe.manifest => cef/src/build_util/win/cef-app.exe.manifest (100%) create mode 100644 cef/src/build_util/win/mod.rs create mode 100644 cef/src/window_info.rs create mode 100644 cef/src/wrapper/browser_info_map.rs create mode 100644 cef/src/wrapper/byte_read_handler.rs create mode 100644 cef/src/wrapper/message_router.rs create mode 100644 cef/src/wrapper/message_router_utils.rs create mode 100644 cef/src/wrapper/mod.rs create mode 100644 cef/src/wrapper/resource_manager.rs create mode 100644 cef/src/wrapper/stream_resource_handler.rs create mode 100644 cef/src/wrapper/zip_archive.rs create mode 100644 examples/cefsimple/build.rs create mode 100644 examples/cefsimple/resources/mac/English.lproj/InfoPlist.strings create mode 100644 examples/cefsimple/resources/mac/English.lproj/MainMenu.xib create mode 100644 examples/cefsimple/resources/mac/cefsimple.icns create mode 100644 examples/cefsimple/resources/win/cefsimple.ico create mode 100644 examples/cefsimple/resources/win/small.ico rename examples/cefsimple/src/{mac/helper.rs => bin/cefsimple_helper.rs} (100%) create mode 100644 examples/cefsimple/src/lib.rs delete mode 100644 examples/cefsimple/src/mac/bundle_cefsimple.rs create mode 100644 examples/cefsimple/src/mac/mod.rs create mode 100644 examples/cefsimple/src/shared/mod.rs create mode 100644 examples/cefsimple/src/shared/resources.rs create mode 100644 examples/cefsimple/src/shared/simple_app.rs create mode 100644 examples/cefsimple/src/shared/simple_handler/linux.rs create mode 100644 examples/cefsimple/src/shared/simple_handler/mac.rs create mode 100644 examples/cefsimple/src/shared/simple_handler/mod.rs create mode 100644 examples/cefsimple/src/shared/simple_handler/win.rs create mode 100644 examples/cefsimple/src/win.rs create mode 100644 examples/tests_shared/Cargo.toml create mode 100644 examples/tests_shared/resources/osr_test.html create mode 100644 examples/tests_shared/resources/pdf.html create mode 100644 examples/tests_shared/resources/pdf.pdf create mode 100644 examples/tests_shared/resources/window_icon.1x.png create mode 100644 examples/tests_shared/resources/window_icon.2x.png create mode 100644 examples/tests_shared/src/browser/client_app_browser.rs create mode 100644 examples/tests_shared/src/browser/file_util.rs create mode 100644 examples/tests_shared/src/browser/geometry_util.rs create mode 100644 examples/tests_shared/src/browser/main_message_loop.rs create mode 100644 examples/tests_shared/src/browser/main_message_loop_external_pump/linux.rs create mode 100644 examples/tests_shared/src/browser/main_message_loop_external_pump/mac.rs create mode 100644 examples/tests_shared/src/browser/main_message_loop_external_pump/mod.rs create mode 100644 examples/tests_shared/src/browser/main_message_loop_external_pump/win.rs create mode 100644 examples/tests_shared/src/browser/main_message_loop_std.rs create mode 100644 examples/tests_shared/src/browser/mod.rs create mode 100644 examples/tests_shared/src/browser/resource_util/mod.rs create mode 100644 examples/tests_shared/src/browser/resource_util/posix.rs create mode 100644 examples/tests_shared/src/browser/resource_util/win.rs create mode 100644 examples/tests_shared/src/browser/util_win.rs create mode 100644 examples/tests_shared/src/common/binary_value_utils.rs create mode 100644 examples/tests_shared/src/common/client_app.rs create mode 100644 examples/tests_shared/src/common/client_app_other.rs create mode 100644 examples/tests_shared/src/common/client_switches.rs create mode 100644 examples/tests_shared/src/common/mod.rs create mode 100644 examples/tests_shared/src/lib.rs create mode 100644 examples/tests_shared/src/process_helper_mac.rs create mode 100644 examples/tests_shared/src/renderer/client_app_renderer.rs create mode 100644 examples/tests_shared/src/renderer/mod.rs diff --git a/.gitignore b/.gitignore index 8bab924..114d01c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ cargo-sources.json Cargo.lock -/.vscode/ \ No newline at end of file +/.vscode/ +**/.DS_Store diff --git a/Cargo.toml b/Cargo.toml index 0772f22..ec5f09f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,18 +23,20 @@ authors = [ repository = "https://github.com/tauri-apps/cef-rs" [workspace.dependencies] -cef = { path = "cef" } +cef = { path = "cef", default-features = false } cef-dll-sys = { version = "143.4.0", path = "sys" } download-cef = { version = "2.2", path = "download-cef" } anyhow = "1" ash = "0.38" bindgen = "0.72" +cargo_metadata = "0.23.1" clap = { version = "4", features = ["derive"] } cmake = "0.1.52" convert_case = "0.10" git-cliff = "2" git-cliff-core = "2" +glib = "0.21" io-surface = "0.16" libc = "0.2" libloading = "0.9" @@ -42,7 +44,8 @@ metal = "0.32" objc = "0.2" objc2 = "0.6.3" objc2-app-kit = { version = "0.3.2", default-features = false } -objc2-io-surface = "0.3" +objc2-foundation = "0.3.2" +objc2-io-surface = "0.3.2" plist = "1" proc-macro2 = "1" quote = "1" @@ -55,6 +58,8 @@ thiserror = "2" toml_edit = "0.24" tracing = "0.1" wgpu = "27" +winres = "0.1" +x11-dl = "2" [workspace.dependencies.windows-sys] version = "0.61" diff --git a/README.md b/README.md index af99317..465cbea 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,11 @@ $env:PATH="$env:PATH;$env:CEF_PATH" ### Run the `cefsimple` Example +This command should work with each platform: +```sh +cargo run --bin bundle-cef-app -- cefsimple -o target/bundle +``` + #### Linux ```sh @@ -61,15 +66,15 @@ cargo run --bin cefsimple #### macOS ```sh -cargo run --bin bundle_cefsimple -open target/debug/cefsimple.app +cargo run --bin bundle-cef-app -- cefsimple -o target/bundle +open target/bundle/cefsimple.app ``` #### Windows (using PowerShell) ```pwsh -cp ./examples/cefsimple/src/win/cefsimple.exe.manifest ./target/debug/ -cargo run --bin cefsimple +cargo run --bin bundle-cef-app -- cefsimple -o ./target/bundle +./target/bundle/cefsimple.exe ``` ## Contributing diff --git a/cef/Cargo.toml b/cef/Cargo.toml index 78f59eb..e74ae70 100644 --- a/cef/Cargo.toml +++ b/cef/Cargo.toml @@ -8,23 +8,44 @@ license.workspace = true authors.workspace = true repository.workspace = true +[lib] + +[[bin]] +name = "bundle-cef-app" + [features] -default = ["sandbox"] +default = ["sandbox", "build-util"] dox = ["cef-dll-sys/dox"] sandbox = ["cef-dll-sys/sandbox"] + # Unified texture import system for CEF hardware acceleration accelerated_osr = [ + "ash", + "libc", + "objc", + "objc2-io-surface", + "metal", + "thiserror", + "tracing", "wgpu", "windows", - "libc", - "ash", - "tracing", - "thiserror", - "objc2-io-surface", - "objc", - "metal" ] +# Build utilities, including the bundle-cef-app tool. +build-util = [ + "anyhow", + "cargo_metadata", + "clap", + "plist", + "semver", + "serde", + "serde_json", + "thiserror", +] + +# Linux X11 support in bundle-cef-app. +linux-x11 = [] + [package.metadata.docs.rs] features = ["dox"] @@ -37,6 +58,13 @@ thiserror = { workspace = true, optional = true } tracing = { workspace = true, optional = true } wgpu = { workspace = true, optional = true } +# bundle +anyhow = { workspace = true, optional = true } +clap = { workspace = true, optional = true } +cargo_metadata = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } + [target.'cfg(target_os = "windows")'.dependencies] windows-sys.workspace = true windows = { workspace = true, optional = true } @@ -47,6 +75,8 @@ objc2.workspace = true objc2-io-surface = { workspace = true, optional = true } objc = { workspace = true, optional = true } metal = { workspace = true, optional = true } +plist = { workspace = true, optional = true } +semver = { workspace = true, optional = true } [target.'cfg(target_os = "linux")'.dependencies] libc = { workspace = true, optional = true } diff --git a/cef/src/args.rs b/cef/src/args.rs index 4f8bf61..15cc48b 100644 --- a/cef/src/args.rs +++ b/cef/src/args.rs @@ -56,9 +56,7 @@ impl Args { #[cfg(any(target_os = "macos", target_os = "linux"))] pub fn as_cmd_line(&self) -> Option { - let Some(cmd_line) = command_line_create() else { - return None; - }; + let cmd_line = command_line_create()?; cmd_line.init_from_argv(self.as_main_args().argc, self.as_main_args().argv.cast()); Some(cmd_line) } @@ -81,3 +79,12 @@ impl Args { cmd_line } } + +impl From for Args { + fn from(main_args: MainArgs) -> Self { + Args { + main_args, + ..Default::default() + } + } +} diff --git a/cef/src/bin/bundle-cef-app/linux.rs b/cef/src/bin/bundle-cef-app/linux.rs new file mode 100644 index 0000000..2ad45e4 --- /dev/null +++ b/cef/src/bin/bundle-cef-app/linux.rs @@ -0,0 +1,26 @@ +use cef::build_util::linux::*; +use clap::Parser; +use std::{env, path::PathBuf}; + +#[derive(Parser, Debug)] +#[command(about, long_about = None)] +struct Args { + name: String, + #[arg(long, default_value_t = false)] + release: bool, + #[arg(short, long)] + output: Option, +} + +pub fn main() -> anyhow::Result<()> { + let args = Args::parse(); + let output = match args.output { + Some(output) => PathBuf::from(output), + None => env::current_dir()?, + }; + + let bundle_path = build_bundle(output.as_path(), &args.name, args.release)?; + let bundle_path = bundle_path.display(); + println!("Run the app from {bundle_path}"); + Ok(()) +} diff --git a/cef/src/bin/bundle-cef-app/mac.rs b/cef/src/bin/bundle-cef-app/mac.rs new file mode 100644 index 0000000..02224a3 --- /dev/null +++ b/cef/src/bin/bundle-cef-app/mac.rs @@ -0,0 +1,46 @@ +use cef::build_util::mac::*; +use clap::Parser; +use semver::Version; +use std::{env, path::PathBuf}; + +#[derive(Parser, Debug)] +#[command(about, long_about = None)] +struct Args { + name: String, + #[arg(short, long)] + output: Option, + #[arg(short, long)] + identifier: Option, + #[arg(short, long)] + display_name: Option, + #[arg(short, long, default_value = "English")] + region: String, + #[arg(short, long, default_value = "1.0.0")] + version: String, +} + +pub fn main() -> anyhow::Result<()> { + let args = Args::parse(); + let output = match args.output { + Some(output) => PathBuf::from(output), + None => env::current_dir()?, + }; + let identifier = args + .identifier + .unwrap_or_else(|| format!("apps.tauri.cef-rs.{}", args.name)); + let display_name = args.display_name.unwrap_or_else(|| args.name.clone()); + let version = Version::parse(&args.version)?; + + let bundle_info = BundleInfo { + name: args.name.clone(), + identifier, + display_name, + development_region: args.region, + version, + }; + + let bundle_path = build_bundle(output.as_path(), &args.name, bundle_info)?; + let bundle_path = bundle_path.display(); + println!("Run the app from {bundle_path}"); + Ok(()) +} diff --git a/cef/src/bin/bundle-cef-app/main.rs b/cef/src/bin/bundle-cef-app/main.rs new file mode 100644 index 0000000..8cc9411 --- /dev/null +++ b/cef/src/bin/bundle-cef-app/main.rs @@ -0,0 +1,29 @@ +#[cfg(not(feature = "build-util"))] +fn main() { + let command = std::env::current_exe().expect("Failed to get current executable path"); + eprintln!("Disabled: {command:?} was compiled without the build-util feature."); +} + +#[cfg(all(feature = "build-util", target_os = "macos"))] +mod mac; + +#[cfg(all(feature = "build-util", target_os = "macos"))] +fn main() -> anyhow::Result<()> { + mac::main() +} + +#[cfg(all(feature = "build-util", target_os = "linux"))] +mod linux; + +#[cfg(all(feature = "build-util", target_os = "linux"))] +fn main() -> anyhow::Result<()> { + linux::main() +} + +#[cfg(all(feature = "build-util", target_os = "windows"))] +mod win; + +#[cfg(all(feature = "build-util", target_os = "windows"))] +fn main() -> anyhow::Result<()> { + win::main() +} diff --git a/cef/src/bin/bundle-cef-app/win.rs b/cef/src/bin/bundle-cef-app/win.rs new file mode 100644 index 0000000..623aa9b --- /dev/null +++ b/cef/src/bin/bundle-cef-app/win.rs @@ -0,0 +1,26 @@ +use cef::build_util::win::*; +use clap::Parser; +use std::{env, path::PathBuf}; + +#[derive(Parser, Debug)] +#[command(about, long_about = None)] +struct Args { + name: String, + #[arg(long, default_value_t = false)] + release: bool, + #[arg(short, long)] + output: Option, +} + +pub fn main() -> anyhow::Result<()> { + let args = Args::parse(); + let output = match args.output { + Some(output) => PathBuf::from(output), + None => env::current_dir()?, + }; + + let bundle_path = build_bundle(output.as_path(), &args.name, args.release)?; + let bundle_path = bundle_path.display(); + println!("Run the app from {bundle_path}"); + Ok(()) +} diff --git a/cef/src/build_util/linux.rs b/cef/src/build_util/linux.rs new file mode 100644 index 0000000..c4507cf --- /dev/null +++ b/cef/src/build_util/linux.rs @@ -0,0 +1,77 @@ +use std::{ + fs, io, + path::{Path, PathBuf}, + process::Command, +}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("I/O error: {0:?}")] + Io(#[from] io::Error), + #[error("Metadata error: {0:?}")] + Metadata(#[from] super::metadata::Error), +} + +pub type Result = std::result::Result; + +/// See https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-linux +pub fn bundle(app_path: &Path, target_path: &Path, executable_name: &str) -> Result { + let cef_path = cef_dll_sys::get_cef_dir().unwrap(); + copy_directory(&cef_path, &app_path)?; + + const LOCALES_DIR: &str = "locales"; + copy_directory(&cef_path.join(LOCALES_DIR), &app_path.join(LOCALES_DIR))?; + + copy_app(app_path, target_path, executable_name) +} + +/// Similar to [`bundle`], but this will invoke `cargo build` to build the executable target. +pub fn build_bundle(app_path: &Path, executable_name: &str, release: bool) -> Result { + let cargo_metadata = super::metadata::get_cargo_metadata()?; + let target_path = + cargo_metadata + .target_directory() + .join(if release { "release" } else { "debug" }); + + cargo_build(executable_name, release)?; + + bundle(app_path, &target_path, executable_name) +} + +fn copy_app(app_path: &Path, target_path: &Path, executable_name: &str) -> Result { + let executable_path = app_path.join(executable_name); + let target_executable = target_path.join(executable_name); + fs::copy(&target_executable, &executable_path)?; + Ok(executable_path) +} + +fn copy_directory(src: &Path, dst: &Path) -> io::Result<()> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let dst_path = dst.join(entry.file_name()); + if entry.file_type()?.is_file() { + fs::copy(entry.path(), &dst_path)?; + } + } + Ok(()) +} + +fn cargo_build(name: &str, release: bool) -> Result<()> { + println!("Building {name}..."); + + let mut args = vec!["build"]; + if release { + args.push("--release"); + } + #[cfg(feature = "linux-x11")] + args.extend(["-F", "linux-x11"]); + args.extend(["--bin", name]); + + let status = Command::new(super::cargo_path()).args(args).status()?; + if status.success() { + Ok(()) + } else { + Err(io::Error::from(io::ErrorKind::Interrupted).into()) + } +} diff --git a/cef/src/build_util/mac.rs b/cef/src/build_util/mac.rs new file mode 100644 index 0000000..2240779 --- /dev/null +++ b/cef/src/build_util/mac.rs @@ -0,0 +1,342 @@ +use semver::Version; +use serde::Serialize; +use std::{ + collections::HashMap, + fs, io, + path::{Path, PathBuf}, + process::Command, +}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("I/O error: {0:?}")] + Io(#[from] io::Error), + #[error("Metadata error: {0:?}")] + Metadata(#[from] super::metadata::Error), + #[error("Plist error: {0:?}")] + Plist(#[from] plist::Error), +} + +pub type Result = std::result::Result; + +/// Common bundle information that is shared in the [Info.plist](https://developer.apple.com/documentation/bundleresources/information-property-list) files. +#[derive(Clone, Serialize)] +pub struct BundleInfo { + /// [CFBundleName](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundlename) + #[serde(rename = "CFBundleName")] + pub name: String, + /// [CFBundleIdentifier](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleidentifier) + #[serde(rename = "CFBundleIdentifier")] + pub identifier: String, + /// [CFBundleDisplayName](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundledisplayname) + #[serde(rename = "CFBundleDisplayName")] + pub display_name: String, + /// [CFBundleDevelopmentRegion](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundledevelopmentregion) + #[serde(rename = "CFBundleDevelopmentRegion")] + pub development_region: String, + /// [CFBundleVersion](https://developer.apple.com/documentation/bundleresources/information-property-list/cfbundleversion) + #[serde(rename = "CFBundleVersion", serialize_with = "serialize_version")] + pub version: Version, +} + +impl BundleInfo { + pub fn new( + name: &str, + identifier: &str, + display_name: &str, + development_region: &str, + version: Version, + ) -> Self { + Self { + name: name.to_owned(), + identifier: identifier.to_owned(), + display_name: display_name.to_owned(), + development_region: development_region.to_owned(), + version, + } + } +} + +/// See https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-macos +pub fn bundle( + app_path: &Path, + target_path: &Path, + executable_name: &str, + helper_name: &str, + resources_path: Option, + bundle_info: BundleInfo, +) -> Result { + let main_app_path = create_app( + app_path, + executable_name, + false, + resources_path.as_deref(), + bundle_info.clone(), + &target_path.join(executable_name), + )?; + let cef_path = cef_dll_sys::get_cef_dir().unwrap(); + let to = main_app_path.join(FRAMEWORKS_PATH).join(FRAMEWORK); + if to.exists() { + fs::remove_dir_all(&to).unwrap(); + } + copy_directory(&cef_path.join(FRAMEWORK), &to)?; + for helper in HELPERS { + let helper = format!("{executable_name} {helper}"); + create_app( + &main_app_path.join(FRAMEWORKS_PATH), + &helper, + true, + None, + bundle_info.clone(), + &target_path.join(helper_name), + )?; + } + if let Some(resources_path) = resources_path { + let resources_path = resources_path.join("mac"); + let target_path = main_app_path.join(RESOURCES_PATH); + copy_app_resources(&resources_path, &target_path)?; + } + Ok(main_app_path) +} + +/// Similar to [`bundle`], but this will invoke `cargo build` to build both the main executable and +/// helper executable targets. +pub fn build_bundle( + app_path: &Path, + executable_name: &str, + bundle_info: BundleInfo, +) -> Result { + let cargo_metadata = super::metadata::get_cargo_metadata()?; + let target_path = cargo_metadata.target_directory().join("debug"); + let bundle_metadata = cargo_metadata.parse_bundle_metadata(executable_name)?; + + cargo_build(executable_name)?; + cargo_build(&bundle_metadata.helper_name)?; + + bundle( + app_path, + &target_path, + executable_name, + &bundle_metadata.helper_name, + bundle_metadata.resources_path, + bundle_info, + ) +} + +#[derive(Serialize)] +struct InfoPlist { + #[serde(flatten)] + bundle_info: BundleInfo, + + #[serde(rename = "CFBundleExecutable")] + executable_name: String, + #[serde(rename = "CFBundleInfoDictionaryVersion")] + bundle_info_dictionary_version: String, + #[serde(rename = "CFBundlePackageType")] + bundle_package_type: String, + #[serde(rename = "CFBundleIconFile", skip_serializing_if = "String::is_empty")] + icon_file: String, + #[serde(rename = "CFBundleSignature")] + bundle_signature: String, + #[serde( + rename = "CFBundleShortVersionString", + serialize_with = "serialize_version" + )] + short_version: Version, + #[serde(rename = "LSEnvironment")] + environment: HashMap, + #[serde(rename = "LSFileQuarantineEnabled")] + file_quarantine_enabled: bool, + #[serde(rename = "LSMinimumSystemVersion")] + minimum_system_version: String, + #[serde( + rename = "LSUIElement", + skip_serializing_if = "Option::is_none", + serialize_with = "serialize_ui_element" + )] + ui_element: Option<&'static str>, + #[serde(rename = "NSBluetoothAlwaysUsageDescription")] + bluetooth_always_usage_description: String, + #[serde(rename = "NSSupportsAutomaticGraphicsSwitching")] + supports_automatic_graphics_switching: bool, + #[serde(rename = "NSWebBrowserPublicKeyCredentialUsageDescription")] + web_browser_publickey_credential_usage_description: String, + #[serde(rename = "NSCameraUsageDescription")] + camera_usage_description: String, + #[serde(rename = "NSMicrophoneUsageDescription")] + microphone_usage_description: String, +} + +impl InfoPlist { + fn new( + executable_name: &str, + is_helper: bool, + icon_file: Option, + bundle_info: BundleInfo, + ) -> Self { + Self { + executable_name: executable_name.to_owned(), + bundle_info_dictionary_version: "6.0".to_owned(), + bundle_package_type: "APPL".to_owned(), + icon_file: icon_file.unwrap_or_default(), + bundle_signature: "????".to_owned(), + short_version: Version::new( + bundle_info.version.major, + bundle_info.version.minor, + bundle_info.version.patch, + ), + environment: [("MallocNanoZone".to_owned(), "0".to_owned())] + .into_iter() + .collect(), + file_quarantine_enabled: true, + minimum_system_version: "11.0".to_owned(), + ui_element: if is_helper { Some("1") } else { None }, + bluetooth_always_usage_description: executable_name.to_owned(), + supports_automatic_graphics_switching: true, + web_browser_publickey_credential_usage_description: executable_name.to_owned(), + camera_usage_description: executable_name.to_owned(), + microphone_usage_description: executable_name.to_owned(), + bundle_info, + } + } +} + +fn serialize_ui_element( + ui_element: &Option<&'static str>, + serializer: S, +) -> std::result::Result +where + S: serde::Serializer, +{ + match ui_element { + Some(element) => serializer.serialize_str(element), + None => unreachable!("None is skipped"), + } +} + +fn serialize_version(version: &Version, serializer: S) -> std::result::Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&version.to_string()) +} + +const EXEC_PATH: &str = "Contents/MacOS"; +const FRAMEWORKS_PATH: &str = "Contents/Frameworks"; +const RESOURCES_PATH: &str = "Contents/Resources"; +const FRAMEWORK: &str = "Chromium Embedded Framework.framework"; +const HELPERS: &[&str] = &[ + "Helper (GPU)", + "Helper (Renderer)", + "Helper (Plugin)", + "Helper (Alerts)", + "Helper", +]; + +fn create_app_layout(app_path: &Path) -> Result { + for path in [EXEC_PATH, RESOURCES_PATH, FRAMEWORKS_PATH] { + fs::create_dir_all(app_path.join(path))?; + } + Ok(app_path.join("Contents")) +} + +fn create_app( + app_path: &Path, + executable_name: &str, + is_helper: bool, + resources_path: Option<&Path>, + bundle_info: BundleInfo, + bin: &Path, +) -> Result { + let app_path = app_path.join(executable_name).with_extension("app"); + let contents_path = create_app_layout(&app_path)?; + let icon_file = resources_path.and_then(|path| { + let icon_file = format!("{executable_name}.icns"); + if path.join("mac").join(&icon_file).exists() { + Some(icon_file) + } else { + None + } + }); + create_info_plist( + &contents_path, + executable_name, + bundle_info, + is_helper, + icon_file, + )?; + let executable_path = app_path.join(EXEC_PATH).join(executable_name); + fs::copy(bin, executable_path)?; + Ok(app_path) +} + +fn create_info_plist( + contents_path: &Path, + executable_name: &str, + bundle_info: BundleInfo, + is_helper: bool, + icon_file: Option, +) -> Result<()> { + let info_plist = InfoPlist::new(executable_name, is_helper, icon_file, bundle_info); + plist::to_file_xml(contents_path.join("Info.plist"), &info_plist)?; + Ok(()) +} + +fn copy_directory(src: &Path, dst: &Path) -> io::Result<()> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let dst_path = dst.join(entry.file_name()); + if entry.file_type()?.is_dir() { + copy_directory(&entry.path(), &dst_path)?; + } else { + fs::copy(entry.path(), &dst_path)?; + } + } + Ok(()) +} + +fn copy_app_resources(src: &Path, dst: &Path) -> io::Result<()> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let mut dst_path = dst.join(entry.file_name()); + if entry.file_type()?.is_dir() { + copy_app_resources(&entry.path(), &dst_path)?; + } else { + let entry = entry.path(); + if entry + .extension() + .map(|ext| ext == "xib") + .unwrap_or_default() + { + dst_path.set_extension("nib"); + let (Some(dst_path), Some(entry)) = (dst_path.to_str(), entry.to_str()) else { + return Err(io::Error::from(io::ErrorKind::NotFound)); + }; + let status = Command::new("xcrun") + .args(["ibtool", "--compile", dst_path, entry]) + .status()?; + if !status.success() { + return Err(io::Error::from(io::ErrorKind::Interrupted)); + } + } else { + fs::copy(entry, &dst_path)?; + } + } + } + Ok(()) +} + +fn cargo_build(name: &str) -> Result<()> { + println!("Building {name}..."); + + let status = Command::new(super::cargo_path()) + .args(["build", "--bin", name]) + .status()?; + if status.success() { + Ok(()) + } else { + Err(io::Error::from(io::ErrorKind::Interrupted).into()) + } +} diff --git a/cef/src/build_util/metadata.rs b/cef/src/build_util/metadata.rs new file mode 100644 index 0000000..d90283e --- /dev/null +++ b/cef/src/build_util/metadata.rs @@ -0,0 +1,86 @@ +use serde::Deserialize; +use std::path::PathBuf; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Cargo metadata error: {0:?}")] + Metadata(#[from] cargo_metadata::Error), + #[error("Missing package metadata for {0}")] + MissingPackageMetadata(String), +} + +pub type Result = std::result::Result; + +#[derive(Deserialize)] +struct PackageMetadata { + cef: Cef, +} + +#[derive(Deserialize)] +struct Cef { + bundle: CargoBundleMetadata, +} + +#[derive(Deserialize)] +struct CargoBundleMetadata { + #[cfg(target_os = "macos")] + helper_name: String, + resources_path: Option, +} + +pub struct BundleMetadata { + #[cfg(target_os = "macos")] + pub helper_name: String, + pub resources_path: Option, +} + +impl BundleMetadata { + pub fn parse(executable: &str, metadata: &cargo_metadata::Metadata) -> Option { + let package = metadata + .packages + .iter() + .find(|p| p.targets.iter().any(|t| t.name == executable))?; + let package_metadata = + serde_json::from_value::(package.metadata.clone()).ok()?; + let resources_path = package_metadata + .cef + .bundle + .resources_path + .as_deref() + .and_then(|resources_path| { + package + .manifest_path + .clone() + .into_std_path_buf() + .parent() + .map(|manifest_dir| manifest_dir.join(resources_path)) + }); + Some(Self { + #[cfg(target_os = "macos")] + helper_name: package_metadata.cef.bundle.helper_name, + resources_path, + }) + } +} + +pub struct CargoMetadata(cargo_metadata::Metadata); + +impl CargoMetadata { + pub fn target_directory(&self) -> PathBuf { + PathBuf::from(&self.0.target_directory) + } + + pub fn parse_bundle_metadata(&self, executable: &str) -> Result { + BundleMetadata::parse(executable, &self.0) + .ok_or_else(|| Error::MissingPackageMetadata(executable.to_owned())) + } +} + +/// Run `cargo metadata` to determine the configuration for the current workspace/package. +pub fn get_cargo_metadata() -> Result { + let metadata = cargo_metadata::MetadataCommand::new() + .no_deps() + .other_options(vec!["--frozen".to_string()]) + .exec()?; + Ok(CargoMetadata(metadata)) +} diff --git a/cef/src/build_util/mod.rs b/cef/src/build_util/mod.rs new file mode 100644 index 0000000..086630b --- /dev/null +++ b/cef/src/build_util/mod.rs @@ -0,0 +1,18 @@ +use std::env; + +pub mod metadata; + +#[cfg(target_os = "macos")] +pub mod mac; + +#[cfg(target_os = "linux")] +pub mod linux; + +#[cfg(target_os = "windows")] +pub mod win; + +/// Prefer the path in the `CARGO` environment variable if specified, otherwise just execute +/// `cargo` from wherever it is found in the `PATH`. +fn cargo_path() -> String { + env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()) +} diff --git a/examples/cefsimple/src/win/cefsimple.exe.manifest b/cef/src/build_util/win/cef-app.exe.manifest similarity index 100% rename from examples/cefsimple/src/win/cefsimple.exe.manifest rename to cef/src/build_util/win/cef-app.exe.manifest diff --git a/cef/src/build_util/win/mod.rs b/cef/src/build_util/win/mod.rs new file mode 100644 index 0000000..a98d1c1 --- /dev/null +++ b/cef/src/build_util/win/mod.rs @@ -0,0 +1,117 @@ +use std::{ + fs, + io::{self, Write}, + path::{Path, PathBuf}, + process::Command, +}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("I/O error: {0:?}")] + Io(#[from] io::Error), + #[error("Metadata error: {0:?}")] + Metadata(#[from] super::metadata::Error), +} + +pub type Result = std::result::Result; + +/// See https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-linux +pub fn bundle(app_path: &Path, target_path: &Path, executable_name: &str) -> Result { + let cef_path = cef_dll_sys::get_cef_dir().unwrap(); + copy_directory(&cef_path, &app_path)?; + + const LOCALES_DIR: &str = "locales"; + copy_directory(&cef_path.join(LOCALES_DIR), &app_path.join(LOCALES_DIR))?; + + copy_app(app_path, target_path, executable_name) +} + +/// Similar to [`bundle`], but this will invoke `cargo build` to build the executable target. +pub fn build_bundle(app_path: &Path, executable_name: &str, release: bool) -> Result { + let cargo_metadata = super::metadata::get_cargo_metadata()?; + let target_path = + cargo_metadata + .target_directory() + .join(if release { "release" } else { "debug" }); + + cargo_build(executable_name, release)?; + + bundle(app_path, &target_path, executable_name) +} + +const MANIFEST_CONTENT: &[u8] = include_bytes!("cef-app.exe.manifest"); + +fn copy_app(app_path: &Path, target_path: &Path, executable_name: &str) -> Result { + let mut manifest_file = + fs::File::create(app_path.join(format!("{executable_name}.exe.manifest")))?; + manifest_file.write_all(MANIFEST_CONTENT)?; + + #[cfg(feature = "sandbox")] + { + let dll_name = format!("{executable_name}.dll"); + let dll_path = app_path.join(&dll_name); + let target_dll = target_path.join(&dll_name); + fs::copy(&target_dll, &dll_path)?; + + let pdb_name = format!("{executable_name}.pdb"); + let pdb_path = app_path.join(&pdb_name); + let target_pdb = target_path.join(&pdb_name); + fs::copy(&target_pdb, &pdb_path)?; + + let executable_name = format!("{executable_name}.exe"); + let executable_path = app_path.join(&executable_name); + let cef_path = cef_dll_sys::get_cef_dir().unwrap(); + let target_executable = cef_path.join("bootstrap.exe"); + fs::copy(&target_executable, &executable_path)?; + + Ok(executable_path) + } + + #[cfg(not(feature = "sandbox"))] + { + let executable_name = format!("{executable_name}.exe"); + let executable_path = app_path.join(&executable_name); + let target_executable = target_path.join(executable_name); + fs::copy(&target_executable, &executable_path)?; + + Ok(executable_path) + } +} + +fn copy_directory(src: &Path, dst: &Path) -> io::Result<()> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + let dst_path = dst.join(entry.file_name()); + if entry.file_type()?.is_file() + && !entry + .path() + .extension() + .map(|ext| ext == "exe") + .unwrap_or_default() + { + fs::copy(entry.path(), &dst_path)?; + } + } + Ok(()) +} + +fn cargo_build(name: &str, release: bool) -> Result<()> { + println!("Building {name}..."); + + let mut args = vec!["build"]; + if release { + args.push("--release"); + } + #[cfg(feature = "sandbox")] + args.extend_from_slice(&["-p", name, "--lib"]); + #[cfg(not(feature = "sandbox"))] + args.extend_from_slice(&["--no-default-features", "--bin", name]); + + let status = Command::new(super::cargo_path()).args(args).status()?; + if status.success() { + Ok(()) + } else { + Err(io::Error::from(io::ErrorKind::Interrupted).into()) + } +} diff --git a/cef/src/lib.rs b/cef/src/lib.rs index e9e5983..6b7477c 100644 --- a/cef/src/lib.rs +++ b/cef/src/lib.rs @@ -3,6 +3,8 @@ pub mod args; pub mod rc; pub mod string; +pub mod window_info; +pub mod wrapper; #[cfg(target_os = "macos")] pub mod application_mac; @@ -16,10 +18,15 @@ pub mod sandbox; #[cfg(feature = "accelerated_osr")] pub mod osr_texture_import; +#[cfg(feature = "build-util")] +pub mod build_util; + #[rustfmt::skip] mod bindings; pub use bindings::*; +pub use rc::Rc as _; + pub use cef_dll_sys as sys; #[cfg(all( @@ -27,3 +34,7 @@ pub use cef_dll_sys as sys; feature = "accelerated_osr" ))] compile_error!("accelerated_osr not supported on this platform"); + +pub const SEEK_SET: i32 = 0; +pub const SEEK_CUR: i32 = 1; +pub const SEEK_END: i32 = 2; diff --git a/cef/src/sandbox.rs b/cef/src/sandbox.rs index f3157b9..dc337b8 100644 --- a/cef/src/sandbox.rs +++ b/cef/src/sandbox.rs @@ -43,6 +43,12 @@ impl Sandbox { } } +impl Default for Sandbox { + fn default() -> Self { + Self::new() + } +} + impl Drop for Sandbox { fn drop(&mut self) { unsafe { diff --git a/cef/src/string.rs b/cef/src/string.rs index 0ca8661..64b9f02 100644 --- a/cef/src/string.rs +++ b/cef/src/string.rs @@ -856,6 +856,7 @@ impl<'a, T> From<&'a mut CefStringCollection> for Option<&'a mut T> { } /// See [_cef_string_list_t] for more documentation. +#[derive(Clone)] pub struct CefStringList(CefStringCollection<_cef_string_list_t>); impl CefStringList { @@ -988,6 +989,7 @@ impl Debug for CefStringList { } /// See [_cef_string_map_t] for more documentation. +#[derive(Clone)] pub struct CefStringMap(CefStringCollection<_cef_string_map_t>); impl CefStringMap { @@ -1134,6 +1136,7 @@ impl Debug for CefStringMap { } /// See [_cef_string_multimap_t] for more documentation. +#[derive(Clone)] pub struct CefStringMultimap(CefStringCollection<_cef_string_multimap_t>); impl CefStringMultimap { diff --git a/cef/src/window_info.rs b/cef/src/window_info.rs new file mode 100644 index 0000000..6bab632 --- /dev/null +++ b/cef/src/window_info.rs @@ -0,0 +1,60 @@ +use crate::{sys::cef_window_handle_t, *}; +#[cfg(target_os = "windows")] +use windows_sys::Win32::UI::WindowsAndMessaging::*; + +impl WindowInfo { + /// Create the browser as a child window. + pub fn set_as_child(self, parent: cef_window_handle_t, bounds: &Rect) -> Self { + Self { + #[cfg(target_os = "windows")] + style: WS_CHILD | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_TABSTOP | WS_VISIBLE, + #[cfg(any(target_os = "linux", target_os = "windows"))] + parent_window: parent, + #[cfg(target_os = "macos")] + parent_view: parent, + bounds: bounds.clone(), + #[cfg(target_os = "macos")] + hidden: 0, + ..self + } + } + + /// Create the browser as a popup window. + #[cfg(target_os = "windows")] + pub fn set_as_popup(self, parent: cef_window_handle_t, title: &str) -> Self { + Self { + window_name: CefString::from(title), + parent_window: parent, + style: WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN | WS_CLIPSIBLINGS | WS_VISIBLE, + bounds: Rect { + x: CW_USEDEFAULT, + y: CW_USEDEFAULT, + width: CW_USEDEFAULT, + height: CW_USEDEFAULT, + }, + ..self + } + } + + /// Create the browser using windowless (off-screen) rendering. No window + /// will be created for the browser and all rendering will occur via the + /// CefRenderHandler interface. The |parent| value will be used to identify + /// monitor info and to act as the parent window for dialogs, context menus, + /// etc. If |parent| is not provided then the main screen monitor will be used + /// and some functionality that requires a parent window may not function + /// correctly. In order to create windowless browsers the + /// CefSettings.windowless_rendering_enabled value must be set to true. + /// Transparent painting is enabled by default but can be disabled by setting + /// CefBrowserSettings.background_color to an opaque value. + pub fn set_as_windowless(self, parent: cef_window_handle_t) -> Self { + Self { + windowless_rendering_enabled: 1, + #[cfg(any(target_os = "linux", target_os = "windows"))] + parent_window: parent, + #[cfg(target_os = "macos")] + parent_view: parent, + runtime_style: RuntimeStyle::ALLOY, + ..self + } + } +} diff --git a/cef/src/wrapper/browser_info_map.rs b/cef/src/wrapper/browser_info_map.rs new file mode 100644 index 0000000..0731109 --- /dev/null +++ b/cef/src/wrapper/browser_info_map.rs @@ -0,0 +1,164 @@ +use std::{collections::BTreeMap, ops::ControlFlow}; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum BrowserInfoMapVisitorResult { + /// Remove the entry from the map. + RemoveEntry, + /// Keep the entry in the map. + KeepEntry, +} + +/// Implement this interface to visit and optionally delete objects in the map. +pub trait BrowserInfoMapVisitor { + fn on_next_info( + &self, + browser_id: i32, + key: K, + value: &V, + ) -> ControlFlow; +} + +#[derive(Default)] +pub struct BrowserInfoMap { + map: BTreeMap>, +} + +impl BrowserInfoMap { + /// Add an object associated with the specified ID values. + pub fn insert(&mut self, browser_id: i32, key: K, value: V) { + self.map.entry(browser_id).or_default().insert(key, value); + } + + /// Find the object with the specified ID values. |visitor| can optionally be + /// used to evaluate or remove the object at the same time. If the object is + /// removed using the Visitor the caller is responsible for destroying it. + pub fn find( + &mut self, + browser_id: i32, + key: K, + visitor: Option<&dyn BrowserInfoMapVisitor>, + ) -> Option { + let info_map = self.map.get_mut(&browser_id)?; + let entry = info_map.get(&key)?; + + if let Some(visitor) = visitor { + let result = match visitor.on_next_info(browser_id, key, entry) { + ControlFlow::Break(result) => result, + ControlFlow::Continue(result) => result, + }; + + if result == BrowserInfoMapVisitorResult::RemoveEntry { + let entry = info_map.remove(&key); + if info_map.is_empty() { + self.map.remove(&browser_id); + } + return entry; + } + } + + Some(entry.clone()) + } + + /// Find all objects. If any objects are removed using the Visitor the caller + /// is responsible for destroying them. + pub fn find_all(&mut self, visitor: &dyn BrowserInfoMapVisitor) { + let browser_ids: Vec<_> = self.map.keys().copied().collect(); + for browser_id in browser_ids { + let info_map = self + .map + .get_mut(&browser_id) + .expect("missing browser info map"); + + let mut keep_going = true; + let mut removed = vec![]; + let keys: Vec<_> = info_map.keys().copied().collect(); + for key in keys { + let value = info_map.get(&key).expect("missing value"); + let result = visitor.on_next_info(browser_id, key, value); + let (stop, result) = match result { + ControlFlow::Break(result) => (true, result), + ControlFlow::Continue(result) => (false, result), + }; + + if result == BrowserInfoMapVisitorResult::RemoveEntry { + removed.push(key); + } + + if stop { + keep_going = false; + break; + } + } + + for key in removed { + info_map.remove(&key); + } + + if info_map.is_empty() { + self.map.remove(&browser_id); + } + + if !keep_going { + break; + } + } + } + + /// Find all objects associated with the specified browser. If any objects are + /// removed using the Visitor the caller is responsible for destroying them. + pub fn find_browser_all(&mut self, browser_id: i32, visitor: &dyn BrowserInfoMapVisitor) { + let info_map = self + .map + .get_mut(&browser_id) + .expect("missing browser info map"); + + let mut removed = vec![]; + let keys: Vec<_> = info_map.keys().copied().collect(); + for key in keys { + let value = info_map.get(&key).expect("missing value"); + let result = visitor.on_next_info(browser_id, key, value); + let (stop, result) = match result { + ControlFlow::Break(result) => (true, result), + ControlFlow::Continue(result) => (false, result), + }; + + if result == BrowserInfoMapVisitorResult::RemoveEntry { + removed.push(key); + } + + if stop { + break; + } + } + + for key in removed { + info_map.remove(&key); + } + + if info_map.is_empty() { + self.map.remove(&browser_id); + } + } + + /// Returns true if the map is empty. + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } + + /// Returns the number of objects in the map. + pub fn len(&self) -> usize { + self.map.values().map(|info_map| info_map.len()).sum() + } + + /// Returns the number of objects in the map that are associated with the + /// specified browser. + pub fn browser_len(&self, browser_id: i32) -> usize { + self.map + .get(&browser_id) + .map_or(0, |info_map| info_map.len()) + } + + pub fn clear(&mut self) { + self.map.clear(); + } +} diff --git a/cef/src/wrapper/byte_read_handler.rs b/cef/src/wrapper/byte_read_handler.rs new file mode 100644 index 0000000..d3499a4 --- /dev/null +++ b/cef/src/wrapper/byte_read_handler.rs @@ -0,0 +1,110 @@ +//! Thread safe implementation of the [ReadHandler] type for reading an in-memory array of bytes. +use crate::*; +use std::sync::{Arc, Mutex}; + +pub struct ByteStream { + bytes: Vec, + offset: usize, +} + +impl ByteStream { + pub fn new(bytes: Vec) -> Self { + ByteStream { bytes, offset: 0 } + } +} + +wrap_read_handler! { + pub struct ByteReadHandler { + stream: Arc>, + } + + impl ReadHandler { + #[allow(clippy::not_unsafe_ptr_arg_deref)] + fn read(&self, ptr: *mut u8, size: usize, n: usize) -> usize { + let Ok(mut stream) = self.stream.lock() else { + return 0; + }; + + let s = (stream.bytes.len() - stream.offset) / size; + let ret = s.min(n); + let buffer = unsafe { std::slice::from_raw_parts_mut(ptr, ret * size) }; + buffer.copy_from_slice(&stream.bytes[stream.offset..stream.offset + ret * size]); + stream.offset += ret * size; + ret + } + + fn seek(&self, offset: i64, whence: ::std::os::raw::c_int) -> ::std::os::raw::c_int { + let Ok(mut stream) = self.stream.lock() else { + return -1; + }; + + const SEEK_SET: i32 = 0; + const SEEK_CUR: i32 = 1; + const SEEK_END: i32 = 2; + + match whence { + SEEK_SET => { + if offset < 0 { + return -1; + } + let offset = offset as usize; + if offset > stream.bytes.len() { + return -1; + } + stream.offset = offset; + 0 + } + SEEK_CUR => { + if offset < 0 { + let offset = -offset as usize; + if offset > stream.offset { + return -1; + } + stream.offset -= offset; + } else { + let offset = offset as usize; + if offset + stream.offset > stream.bytes.len() { + return -1; + } + stream.offset += offset; + } + 0 + } + SEEK_END => { + if offset > 0 { + return -1; + } + let offset = -offset as usize; + if offset > stream.bytes.len() { + return -1; + } + stream.offset = stream.bytes.len() - offset; + 0 + } + _ => -1, + } + } + + fn tell(&self) -> i64 { + let Ok(stream) = self.stream.lock() else { + return 0; + }; + stream.offset as i64 + } + + fn eof(&self) -> i32 { + let Ok(stream) = self.stream.lock() else { + return 1; + }; + if stream.offset >= stream.bytes.len() { + 1 + } else { + 0 + } + } + + fn may_block(&self) -> i32 { + 0 + } + } +} diff --git a/cef/src/wrapper/message_router.rs b/cef/src/wrapper/message_router.rs new file mode 100644 index 0000000..f0487cf --- /dev/null +++ b/cef/src/wrapper/message_router.rs @@ -0,0 +1,1773 @@ +use super::{browser_info_map::*, message_router_utils as mru}; +use crate::*; +use std::{ + collections::{BTreeMap, VecDeque}, + ops::{AddAssign, ControlFlow, Range}, + rc::Rc, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, Mutex, OnceLock, Weak, + }, +}; + +/// Used to configure the query router. The same values must be passed to both +/// [MessageRouterBrowserSide] and [MessageRouterRendererSide]. If using +/// multiple router pairs make sure to choose values that do not conflict. +#[derive(Clone, Debug)] +pub struct MessageRouterConfig { + /// Name of the JavaScript function that will be added to the 'window' object + /// for sending a query. The default value is "cefQuery". + pub js_query_function: String, + /// Name of the JavaScript function that will be added to the 'window' object + /// for canceling a pending query. The default value is "cefQueryCancel". + pub js_cancel_function: String, + /// Messages of size (in bytes) larger than this threshold will be sent via + /// shared memory region. + pub message_size_threshold: usize, +} + +impl MessageRouterConfig { + pub const fn validate(&self) -> bool { + !self.js_query_function.is_empty() && !self.js_cancel_function.is_empty() + } +} + +impl Default for MessageRouterConfig { + fn default() -> Self { + Self { + js_query_function: "cefQuery".to_string(), + js_cancel_function: "cefQueryCancel".to_string(), + message_size_threshold: RESPONSE_SIZE_THRESHOLD, + } + } +} + +/// This trait acts as a container for managing binary data. It retains +/// references to the underlying backing store, ensuring it is valid as long as +/// the BinaryBuffer exists. This allows efficient, zero-copy access to data +/// received from another process. +pub trait BinaryBuffer: Send { + /// Returns the read-only pointer to the memory. + fn data(&self) -> &[u8]; + /// Returns the writable pointer to the memory. + fn data_mut(&mut self) -> &mut [u8]; +} + +/// Callback associated with a single pending asynchronous query. Execute the +/// Success or Failure method to send an asynchronous response to the +/// associated JavaScript handler. It is a runtime error to destroy a Callback +/// object associated with an uncanceled query without first executing one of +/// the callback methods. The methods of this class may be called on any +/// browser process thread. +pub trait BrowserSideCallback: Send + Sync { + /// Notify the associated JavaScript onSuccess callback that the query has + /// completed successfully with the specified string |response|. + fn success_str(&self, response: &str); + /// Notify the associated JavaScript onSuccess callback that the query has + /// completed successfully with binary data. + fn success_binary(&self, data: &[u8]); + /// Notify the associated JavaScript onFailure callback that the query has + /// failed with the specified |error_code| and |error_message|. + fn failure(&self, error_code: i32, error_message: &str); +} + +/// Implement this interface to handle queries. All methods will be executed +/// on the browser process UI thread. +pub trait BrowserSideHandler: Send + Sync { + /// Executed when a new query is received. |query_id| uniquely identifies + /// the query for the life span of the router. Return true to handle the + /// query or false to propagate the query to other registered handlers, if + /// any. If no handlers return true from this method then the query will be + /// automatically canceled with an error code of -1 delivered to the + /// JavaScript onFailure callback. If this method returns true then a + /// Callback method must be executed either in this method or asynchronously + /// to complete the query. + fn on_query_str( + &self, + _browser: Option, + _frame: Option, + _query_id: i64, + _request: &str, + _persistent: bool, + _callback: Arc>, + ) -> bool { + false + } + + /// Executed when a new query is received. |query_id| uniquely identifies + /// the query for the life span of the router. Return true to handle the + /// query or false to propagate the query to other registered handlers, if + /// any. If no handlers return true from this method then the query will be + /// automatically canceled with an error code of -1 delivered to the + /// JavaScript onFailure callback. If this method returns true then a + /// Callback method must be executed either in this method or asynchronously + /// to complete the query. + fn on_query_binary( + &self, + _browser: Option, + _frame: Option, + _query_id: i64, + _request: &dyn BinaryBuffer, + _persistent: bool, + _callback: Arc>, + ) -> bool { + false + } + + /// Executed when a query has been canceled either explicitly using the + /// JavaScript cancel function or implicitly due to browser destruction, + /// navigation or renderer process termination. It will only be called for + /// the single handler that returned true from OnQuery for the same + /// |query_id|. No references to the associated Callback object should be + /// kept after this method is called, nor should any Callback methods be + /// executed. + fn on_query_canceled(&self, _browser: Option, _frame: Option, _query_id: i64) {} +} + +/// Implements the browser side of query routing. The methods of this trait may +/// be called on any browser process thread unless otherwise indicated. +pub trait MessageRouterBrowserSide { + type Callback: BrowserSideCallback; + + /// Create a new router with the specified configuration. + fn new(config: MessageRouterConfig) -> Arc; + + /// Add a new query handler. If |first| is true it will be added as the first + /// handler, otherwise it will be added as the last handler. Must be called on + /// the browser process UI thread. + fn add_handler(&self, handler: Arc, first: bool) -> Option; + + /// Remove an existing query handler. Any pending queries associated with the + /// handler will be canceled. Handler::OnQueryCanceled will be called and the + /// associated JavaScript onFailure callback will be executed with an error + /// code of -1. Returns true if the handler is removed successfully or false + /// if the handler is not found. Must be called on the browser process UI + /// thread. + fn remove_handler(&self, handler_id: HandlerId) -> bool; + + /// Cancel all pending queries associated with either |browser| or |handler|. + /// If both |browser| and |handler| are NULL all pending queries will be + /// canceled. Handler::OnQueryCanceled will be called and the associated + /// JavaScript onFailure callback will be executed in all cases with an error + /// code of -1. + fn cancel_pending(&self, browser: Option, handler_id: Option); + + /// Returns the number of queries currently pending for the specified + /// |browser| and/or |handler|. Either or both values may be empty. Must be + /// called on the browser process UI thread. + fn pending_count(&self, browser: Option, handler_id: Option) -> usize; +} + +/// The below methods should be called from other CEF handlers. They must be +/// called exactly as documented for the router to function correctly. +pub trait MessageRouterBrowserSideHandlerCallbacks: MessageRouterBrowserSide { + /// Call from CefLifeSpanHandler::OnBeforeClose. Any pending queries + /// associated with |browser| will be canceled and Handler::OnQueryCanceled + /// will be called. No JavaScript callbacks will be executed since this + /// indicates destruction of the browser. + fn on_before_close(&self, browser: Option); + + /// Call from CefRequestHandler::OnRenderProcessTerminated. Any pending + /// queries associated with |browser| will be canceled and + /// Handler::OnQueryCanceled will be called. No JavaScript callbacks will be + /// executed since this indicates destruction of the context. + fn on_render_process_terminated(&self, browser: Option); + + /// Call from CefRequestHandler::OnBeforeBrowse only if the navigation is + /// allowed to proceed. If |frame| is the main frame then any pending queries + /// associated with |browser| will be canceled and Handler::OnQueryCanceled + /// will be called. No JavaScript callbacks will be executed since this + /// indicates destruction of the context. + fn on_before_browse(&self, browser: Option, frame: Option); + + /// Call from CefClient::OnProcessMessageReceived. Returns true if the message + /// is handled by this router or false otherwise. + fn on_process_message_received( + &self, + browser: Option, + frame: Option, + source_process: ProcessId, + message: Option, + ) -> bool; +} + +/// Implements the renderer side of query routing. The methods of this class +/// must be called on the render process main thread. +pub trait MessageRouterRendererSide { + /// Create a new router with the specified configuration. + fn new(config: MessageRouterConfig) -> Arc; + + /// Returns the number of queries currently pending for the specified + /// |browser| and/or |context|. Either or both values may be empty. + fn pending_count(&self, browser: Option, context: Option) -> usize; +} + +/// The below methods should be called from other CEF handlers. They must be +/// called exactly as documented for the router to function correctly. +pub trait MessageRouterRendererSideHandlerCallbacks: MessageRouterRendererSide { + /// Call from CefRenderProcessHandler::OnContextCreated. Registers the + /// JavaScripts functions with the new context. + fn on_context_created( + &self, + browser: Option, + frame: Option, + context: Option, + ); + + /// Call from CefRenderProcessHandler::OnContextReleased. Any pending queries + /// associated with the released context will be canceled and + /// Handler::OnQueryCanceled will be called in the browser process. + fn on_context_released( + &self, + browser: Option, + frame: Option, + context: Option, + ); + + /// Call from CefRenderProcessHandler::OnProcessMessageReceived. Returns true + /// if the message is handled by this router or false otherwise. + fn on_process_message_received( + &self, + browser: Option, + frame: Option, + source_process: Option, + message: Option, + ) -> bool; +} + +/// ID value reserved for internal use. +const RESERVED_ID: i32 = 0; + +/// Appended to the JS function name for related IPC messages. +const MESSAGE_SUFFIX: &str = "Msg"; + +/// JS object member argument names for cefQuery. +struct ObjectMember; + +impl ObjectMember { + const REQUEST: &str = "request"; + const ON_SUCCESS: &str = "onSuccess"; + const ON_FAILURE: &str = "onFailure"; + const PERSISTENT: &str = "persistent"; +} + +/// Default error information when a query is canceled. +struct CanceledError; + +impl CanceledError { + const CODE: i32 = -1; + const MESSAGE: &str = "The query has been canceled"; +} + +/// Value of 16KB is chosen as a result of performance tests available at +/// http://tests/ipc_performance +const RESPONSE_SIZE_THRESHOLD: usize = 16 * 1024; + +/// A helper template for generating ID values. +struct IdGenerator +where + T: Copy + AddAssign + Ord + From, +{ + next_id: T, + range: Range, +} + +impl IdGenerator +where + T: Copy + AddAssign + Ord + From, +{ + fn new(max: T) -> Self { + let next_id = RESERVED_ID.into(); + Self { + next_id, + range: next_id..max, + } + } + + fn next(&mut self) -> T { + if self.next_id >= self.range.end { + self.next_id = self.range.start; + } + + self.next_id += 1_i32.into(); + self.next_id + } +} + +pub type HandlerId = i32; + +pub struct BrowserSideRouterCallback { + weak_callback: Weak>, + router: Option>, + browser_id: i32, + query_id: i64, + persistent: bool, + message_size_threshold: usize, + query_message_name: String, +} + +impl BrowserSideRouterCallback { + fn new( + router: Arc, + browser_id: i32, + query_id: i64, + persistent: bool, + message_size_threshold: usize, + query_message_name: String, + ) -> Arc> { + Arc::new_cyclic(|weak_self| { + Mutex::new(Self { + weak_callback: weak_self.clone(), + router: Some(router), + browser_id, + query_id, + persistent, + message_size_threshold, + query_message_name, + }) + }) + } + + fn detach(&mut self) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + self.router = None; + } +} + +impl BrowserSideCallback for BrowserSideRouterCallback { + fn success_str(&self, response: &str) { + let builder = mru::create_browser_response_builder( + self.message_size_threshold, + &self.query_message_name, + mru::MessagePayload::from(response), + ); + + // We need to post task here for two reasons: + // 1) To safely access member variables. + // 2) To let the router to persist the query information before + // the Success callback is executed. + let mut task = BrowserSideRouterCallbackSuccess::new(self.weak_callback.clone(), builder); + post_task(ThreadId::UI, Some(&mut task)); + } + + fn success_binary(&self, data: &[u8]) { + let builder = mru::create_browser_response_builder( + self.message_size_threshold, + &self.query_message_name, + mru::MessagePayload::from(data), + ); + + // We need to post task here for two reasons: + // 1) To safely access member variables. + // 2) To let the router to persist the query information before + // the Success callback is executed. + let mut task = BrowserSideRouterCallbackSuccess::new(self.weak_callback.clone(), builder); + post_task(ThreadId::UI, Some(&mut task)); + } + + fn failure(&self, error_code: i32, error_message: &str) { + // We need to post task here for two reasons: + // 1) To safely access member variables. + // 2) To give previosly submitted tasks by the Success calls to execute + // before we invalidate the callback. + let mut task = BrowserSideRouterCallbackFailure::new( + self.weak_callback.clone(), + error_code, + error_message.to_string(), + ); + post_task(ThreadId::UI, Some(&mut task)); + } +} + +wrap_task! { + struct BrowserSideRouterCallbackSuccess { + weak_callback: Weak>, + builder: Rc, + } + + impl Task { + fn execute(&self) { + let Some(callback) = self.weak_callback.upgrade() else { + return; + }; + let Ok(mut callback) = callback.lock() else { + return; + }; + + let router = if callback.persistent { + callback.router.clone() + } else { + // Non-persistent callbacks are only good for a single use. + callback.router.take() + }; + let Some(router) = router else { + return; + }; + + router.on_callback_success(callback.browser_id, callback.query_id, &*self.builder); + } + } +} + +wrap_task! { + struct BrowserSideRouterCallbackFailure { + weak_callback: Weak>, + error_code: i32, + error_message: String, + } + + impl Task { + fn execute(&self) { + let Some(callback) = self.weak_callback.upgrade() else { + return; + }; + let Ok(mut callback) = callback.lock() else { + return; + }; + + // Failure always invalidates the callback. + let router = callback.router.take(); + let Some(router) = router else { + return; + }; + + router.on_callback_failure(callback.browser_id, callback.query_id, self.error_code, &self.error_message); + } + } +} + +/// Structure representing a pending query. +#[derive(Clone, Default)] +struct BrowserSideQueryInfo { + // Browser and frame originated the query. + browser: Option, + frame: Option, + // IDs that uniquely identify the query in the renderer process. These + // values are opaque to the browser process but must be returned with the + // response. + context_id: i32, + request_id: i32, + // True if the query is persistent. + is_persistent: bool, + // Callback associated with the query that must be detached when the query + // is canceled. + callback: Option>>, + // Handler that should be notified if the query is automatically canceled. + handler: Option<(HandlerId, Arc)>, +} + +#[derive(Default, Clone)] +struct HandlersDeque { + offset: HandlerId, + handlers: VecDeque>>, +} + +pub struct BrowserSideRouter { + weak_self: Weak, + config: MessageRouterConfig, + query_message_name: String, + cancel_message_name: String, + query_id_generator: Mutex>, + handlers: Mutex, + browser_query_info_map: Mutex>, +} + +impl BrowserSideRouter { + /// Retrieve a QueryInfo object from the map based on the browser-side query + /// ID. If |always_remove| is true then the QueryInfo object will always be + /// removed from the map. Othewise, the QueryInfo object will only be removed + /// if the query is non-persistent. If |removed| is true the caller is + /// responsible for deleting the returned QueryInfo object. + fn get_query_info( + &self, + browser_id: i32, + query_id: i64, + always_remove: bool, + ) -> Option { + let Ok(mut browser_query_info_map) = self.browser_query_info_map.lock() else { + return None; + }; + + struct Visitor { + always_remove: bool, + } + + impl BrowserInfoMapVisitor for Visitor { + fn on_next_info( + &self, + _browser_id: i32, + _key: i64, + value: &BrowserSideQueryInfo, + ) -> ControlFlow { + ControlFlow::Continue(if self.always_remove || !value.is_persistent { + BrowserInfoMapVisitorResult::RemoveEntry + } else { + BrowserInfoMapVisitorResult::KeepEntry + }) + } + } + + let visitor = Visitor { always_remove }; + browser_query_info_map.find(browser_id, query_id, Some(&visitor)) + } + + /// Called by CallbackImpl on success. + fn on_callback_success( + &self, + browser_id: i32, + query_id: i64, + builder: &dyn mru::ProcessMessageBuilder, + ) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + if let Some(info) = self.get_query_info(browser_id, query_id, false) { + self.send_query_success( + info.browser, + info.frame, + info.context_id, + info.request_id, + builder, + ) + } + } + + /// Called by CallbackImpl on failure. + fn on_callback_failure( + &self, + browser_id: i32, + query_id: i64, + error_code: i32, + error_message: &str, + ) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + if let Some(info) = self.get_query_info(browser_id, query_id, false) { + self.send_query_failure( + info.browser, + info.frame, + info.context_id, + info.request_id, + error_code, + error_message, + ) + } + } + + fn send_query_success( + &self, + _browser: Option, + frame: Option, + context_id: i32, + request_id: i32, + builder: &dyn mru::ProcessMessageBuilder, + ) { + let (Some(frame), Some(mut message)) = ( + frame, + builder.build_browser_response(context_id, request_id), + ) else { + return; + }; + + frame.send_process_message(ProcessId::RENDERER, Some(&mut message)); + } + + fn send_query_failure( + &self, + _browser: Option, + frame: Option, + context_id: i32, + request_id: i32, + error_code: i32, + error_message: &str, + ) { + let (Some(frame), Some(mut message)) = ( + frame, + process_message_create(Some(&CefString::from(self.query_message_name.as_str()))), + ) else { + return; + }; + let Some(args) = message.argument_list() else { + return; + }; + + args.set_int(0, context_id); + args.set_int(1, request_id); + args.set_bool(2, 0); // Indicates a failure result. + args.set_int(3, error_code); + args.set_string(4, Some(&CefString::from(error_message))); + + frame.send_process_message(ProcessId::RENDERER, Some(&mut message)); + } + + /// Cancel a query that has not been sent to a handler. + fn cancel_unhandled_query( + &self, + browser: Option, + frame: Option, + context_id: i32, + request_id: i32, + ) { + self.send_query_failure( + browser, + frame, + context_id, + request_id, + CanceledError::CODE, + CanceledError::MESSAGE, + ); + } + + /// Cancel a query that has already been sent to a handler. + fn cancel_query(&self, query_id: i64, query_info: BrowserSideQueryInfo, notify_renderer: bool) { + if notify_renderer { + self.send_query_failure( + query_info.browser.clone(), + query_info.frame.clone(), + query_info.context_id, + query_info.request_id, + CanceledError::CODE, + CanceledError::MESSAGE, + ); + } + + if let Some((_, handler)) = query_info.handler { + handler.on_query_canceled(query_info.browser, query_info.frame, query_id); + } + + // Invalidate the callback. + if let Some(callback) = query_info.callback { + if let Ok(mut callback) = callback.lock() { + callback.detach(); + } + } + } + + /// Cancel all pending queries associated with either |browser| or |handler|. + /// If both |browser| and |handler| are NULL all pending queries will be + /// canceled. Set |notify_renderer| to true if the renderer should be notified. + fn cancel_pending_for( + &self, + browser: Option, + handler: Option<(HandlerId, Arc)>, + notify_renderer: bool, + ) { + if currently_on(ThreadId::UI) == 0 { + let mut task = BrowserSideRouterCancelPendingFor::new( + self.weak_self.clone(), + browser, + handler, + notify_renderer, + ); + post_task(ThreadId::UI, Some(&mut task)); + return; + } + + let (Some(router), Ok(mut browser_query_info_map)) = + (self.weak_self.upgrade(), self.browser_query_info_map.lock()) + else { + return; + }; + + struct Visitor { + router: Arc, + handler_id: Option, + notify_renderer: bool, + } + + impl BrowserInfoMapVisitor for Visitor { + fn on_next_info( + &self, + _browser_id: i32, + key: i64, + value: &BrowserSideQueryInfo, + ) -> ControlFlow { + ControlFlow::Continue(match (self.handler_id, &value.handler) { + (Some(handler_id), Some(entry)) if handler_id != entry.0 => { + BrowserInfoMapVisitorResult::KeepEntry + } + _ => { + self.router + .cancel_query(key, value.clone(), self.notify_renderer); + BrowserInfoMapVisitorResult::RemoveEntry + } + }) + } + } + + let visitor = Visitor { + router, + handler_id: handler.map(|handler| handler.0), + notify_renderer, + }; + + if let Some(browser) = browser { + // Cancel all queries associated with the specified browser. + browser_query_info_map.find_browser_all(browser.identifier(), &visitor); + } else { + // Cancel all queries for all browsers. + browser_query_info_map.find_all(&visitor); + } + } + + /// Cancel a query based on the renderer-side IDs. If |request_id| is + /// kReservedId all requests associated with |context_id| will be canceled. + fn cancel_pending_request(&self, browser_id: i32, context_id: i32, request_id: i32) { + let (Some(router), Ok(mut browser_query_info_map)) = + (self.weak_self.upgrade(), self.browser_query_info_map.lock()) + else { + return; + }; + + struct Visitor { + router: Arc, + context_id: i32, + request_id: i32, + } + + impl BrowserInfoMapVisitor for Visitor { + fn on_next_info( + &self, + _browser_id: i32, + key: i64, + value: &BrowserSideQueryInfo, + ) -> ControlFlow { + if value.context_id == self.context_id && self.request_id == RESERVED_ID + || value.request_id == self.request_id + { + self.router.cancel_query(key, value.clone(), false); + + if self.request_id == RESERVED_ID { + ControlFlow::Continue(BrowserInfoMapVisitorResult::RemoveEntry) + } else { + // Stop iterating if only canceling a single request. + ControlFlow::Break(BrowserInfoMapVisitorResult::RemoveEntry) + } + } else { + ControlFlow::Continue(BrowserInfoMapVisitorResult::KeepEntry) + } + } + } + + let visitor = Visitor { + router, + context_id, + request_id, + }; + + browser_query_info_map.find_browser_all(browser_id, &visitor); + } +} + +impl MessageRouterBrowserSide for BrowserSideRouter { + type Callback = BrowserSideRouterCallback; + + /// Create a new router with the specified configuration. + fn new(config: MessageRouterConfig) -> Arc { + Arc::new_cyclic(|weak_self| { + let query_message_name = format!("{}{MESSAGE_SUFFIX}", config.js_query_function); + let cancel_message_name = format!("{}{MESSAGE_SUFFIX}", config.js_cancel_function); + Self { + weak_self: weak_self.clone(), + config, + query_message_name, + cancel_message_name, + query_id_generator: Mutex::new(IdGenerator::new(i64::MAX)), + handlers: Default::default(), + browser_query_info_map: Default::default(), + } + }) + } + + /// Add a new query handler. If |first| is true it will be added as the first + /// handler, otherwise it will be added as the last handler. Must be called on + /// the browser process UI thread. + fn add_handler(&self, handler: Arc, first: bool) -> Option { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + let handler_id = if first { + let mut handlers = self.handlers.lock().ok()?; + handlers.offset += 1; + handlers.handlers.push_front(Some(handler)); + -handlers.offset + } else { + let mut handlers = self.handlers.lock().ok()?; + let offset = i32::try_from(handlers.handlers.len()).ok()? - handlers.offset; + handlers.handlers.push_back(Some(handler)); + offset + }; + + Some(handler_id) + } + + /// Remove an existing query handler. Any pending queries associated with the + /// handler will be canceled. Handler::OnQueryCanceled will be called and the + /// associated JavaScript onFailure callback will be executed with an error + /// code of -1. Returns true if the handler is removed successfully or false + /// if the handler is not found. Must be called on the browser process UI + /// thread. + fn remove_handler(&self, handler_id: HandlerId) -> bool { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + let Ok(mut handlers) = self.handlers.lock() else { + return false; + }; + let Ok(index) = usize::try_from(handler_id + handlers.offset) else { + return false; + }; + let Some(entry) = handlers.handlers.get_mut(index) else { + return false; + }; + let handler = entry.take(); + + let trim_start = handlers + .handlers + .iter() + .take_while(|entry| entry.is_none()) + .count(); + for _ in 0..trim_start { + handlers.offset -= 1; + handlers.handlers.pop_front(); + } + + let trim_end = handlers + .handlers + .iter() + .rev() + .take_while(|entry| entry.is_none()) + .count(); + for _ in 0..trim_end { + handlers.handlers.pop_back(); + } + + if let Some(handler) = handler { + self.cancel_pending_for(None, Some((handler_id, handler)), true); + true + } else { + false + } + } + + /// Cancel all pending queries associated with either |browser| or |handler|. + /// If both |browser| and |handler| are NULL all pending queries will be + /// canceled. Handler::OnQueryCanceled will be called and the associated + /// JavaScript onFailure callback will be executed in all cases with an error + /// code of -1. + fn cancel_pending(&self, browser: Option, handler_id: Option) { + let handler = handler_id.and_then(|handler_id| { + let handlers = self.handlers.lock().ok()?; + let index = usize::try_from(handler_id + handlers.offset).ok()?; + let entry = handlers.handlers.get(index)?; + entry.as_ref().map(|handler| (handler_id, handler.clone())) + }); + self.cancel_pending_for(browser, handler, true); + } + + /// Returns the number of queries currently pending for the specified + /// |browser| and/or |handler|. Either or both values may be empty. Must be + /// called on the browser process UI thread. + fn pending_count(&self, browser: Option, handler_id: Option) -> usize { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + let Ok(mut browser_query_info_map) = self.browser_query_info_map.lock() else { + return 0; + }; + if browser_query_info_map.is_empty() { + return 0; + } + + let handler_id = handler_id.and_then(|handler_id| { + let handlers = self.handlers.lock().ok()?; + let index = usize::try_from(handler_id + handlers.offset).ok()?; + let entry = handlers.handlers.get(index)?; + entry.as_ref().map(|_| handler_id) + }); + + if let Some(handler_id) = handler_id { + // Need to iterate over each QueryInfo object to test the handler. + struct Visitor { + handler_id: HandlerId, + count: AtomicUsize, + } + + impl BrowserInfoMapVisitor for Visitor { + fn on_next_info( + &self, + _browser_id: i32, + _key: i64, + value: &BrowserSideQueryInfo, + ) -> ControlFlow + { + if value + .handler + .as_ref() + .map(|handler| handler.0 == self.handler_id) + .unwrap_or(false) + { + self.count.fetch_add(1, Ordering::Relaxed); + } + + ControlFlow::Continue(BrowserInfoMapVisitorResult::KeepEntry) + } + } + + let visitor = Visitor { + handler_id, + count: Default::default(), + }; + + if let Some(browser) = browser { + browser_query_info_map.find_browser_all(browser.identifier(), &visitor); + } else { + browser_query_info_map.find_all(&visitor); + } + + visitor.count.load(Ordering::Relaxed) + } else if let Some(browser) = browser { + // Count queries associated with the specified browser. + browser_query_info_map.browser_len(browser.identifier()) + } else { + // Count all queries for all browsers. + browser_query_info_map.len() + } + } +} + +impl MessageRouterBrowserSideHandlerCallbacks for BrowserSideRouter { + /// Call from CefLifeSpanHandler::OnBeforeClose. Any pending queries + /// associated with |browser| will be canceled and Handler::OnQueryCanceled + /// will be called. No JavaScript callbacks will be executed since this + /// indicates destruction of the browser. + fn on_before_close(&self, browser: Option) { + self.cancel_pending_for(browser, None, false); + } + + /// Call from CefRequestHandler::OnRenderProcessTerminated. Any pending + /// queries associated with |browser| will be canceled and + /// Handler::OnQueryCanceled will be called. No JavaScript callbacks will be + /// executed since this indicates destruction of the context. + fn on_render_process_terminated(&self, browser: Option) { + self.cancel_pending_for(browser, None, false); + } + + /// Call from CefRequestHandler::OnBeforeBrowse only if the navigation is + /// allowed to proceed. If |frame| is the main frame then any pending queries + /// associated with |browser| will be canceled and Handler::OnQueryCanceled + /// will be called. No JavaScript callbacks will be executed since this + /// indicates destruction of the context. + fn on_before_browse(&self, browser: Option, frame: Option) { + if frame.map(|frame| frame.is_main() != 0).unwrap_or(false) { + self.cancel_pending_for(browser, None, false); + } + } + + /// Call from CefClient::OnProcessMessageReceived. Returns true if the message + /// is handled by this router or false otherwise. + fn on_process_message_received( + &self, + browser: Option, + frame: Option, + _source_process: ProcessId, + message: Option, + ) -> bool { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + let Some(message) = message else { + return true; + }; + + let name = CefString::from(&message.name()).to_string(); + if name == self.query_message_name { + let content = mru::RenderMessage::from(Some(message)); + + let (arc_self, browser_id, query_id, handlers, mut browser_query_info_map) = match ( + self.weak_self.upgrade(), + browser.as_ref(), + self.query_id_generator.lock(), + self.handlers.lock(), + self.browser_query_info_map.lock(), + ) { + ( + Some(arc_self), + Some(browser), + Ok(mut query_id_generator), + Ok(handlers), + Ok(browser_query_info_map), + ) if !handlers.handlers.is_empty() => ( + arc_self, + browser.identifier(), + query_id_generator.next(), + handlers.clone(), + browser_query_info_map, + ), + _ => { + self.cancel_unhandled_query( + browser, + frame, + content.context_id, + content.request_id, + ); + return true; + } + }; + + let callback = BrowserSideRouterCallback::new( + arc_self, + browser_id, + query_id, + content.is_persistent, + self.config.message_size_threshold, + name, + ); + + let invoke_handler = { + Box::new( + |handler: &dyn BrowserSideHandler, payload: &mru::MessagePayload| match payload + { + mru::MessagePayload::Empty => handler.on_query_binary( + browser.clone(), + frame.clone(), + query_id, + &mru::EmptyBinaryBuffer, + content.is_persistent, + callback.clone(), + ), + mru::MessagePayload::String(payload) => handler.on_query_str( + browser.clone(), + frame.clone(), + query_id, + &payload.to_string(), + content.is_persistent, + callback.clone(), + ), + mru::MessagePayload::Binary(buffer) => handler.on_query_binary( + browser.clone(), + frame.clone(), + query_id, + buffer.as_ref(), + content.is_persistent, + callback.clone(), + ), + }, + ) + }; + let handler = handlers + .handlers + .iter() + .enumerate() + .find_map(move |(index, handler)| { + let handler_id = i32::try_from(index).ok()? - handlers.offset; + let handler = handler.as_ref()?; + if invoke_handler(handler.as_ref(), &content.payload) { + Some((handler_id, handler.clone())) + } else { + None + } + }); + if let Some(handler) = handler { + // Persist the query information until the callback executes. + // It's safe to do this here because the callback will execute + // asynchronously. + let query_info = BrowserSideQueryInfo { + browser, + frame, + context_id: content.context_id, + request_id: content.request_id, + is_persistent: content.is_persistent, + callback: Some(callback), + handler: Some(handler), + }; + + browser_query_info_map.insert(browser_id, query_id, query_info); + } else { + if let Ok(mut callback) = callback.lock() { + callback.detach(); + } + + // No one chose to handle the query so cancel it. + self.cancel_unhandled_query(browser, frame, content.context_id, content.request_id); + } + + return true; + } else if name == self.cancel_message_name { + let (browser, args) = match (browser, message.argument_list()) { + (Some(browser), Some(args)) => (browser, args), + _ => return true, + }; + debug_assert_eq!(args.size(), 2); + + let browser_id = browser.identifier(); + let context_id = args.int(0); + let request_id = args.int(1); + + self.cancel_pending_request(browser_id, context_id, request_id); + return true; + } + + false + } +} + +wrap_task! { + struct BrowserSideRouterCancelPendingFor { + router: Weak, + browser: Option, + handler: Option<(HandlerId, Arc)>, + notify_renderer: bool, + } + + impl Task { + fn execute(&self) { + let Some(router) = self.router.upgrade() else { + return; + }; + + router.cancel_pending_for(self.browser.clone(), self.handler.clone(), self.notify_renderer); + } + } +} + +/// Structure representing a pending request. +#[derive(Clone, Default)] +pub struct RendererSideRequestInfo { + /// True if the request is persistent. + is_persistent: bool, + /// Success callback function. May be [`None`]. + success_callback: Option, + /// Failure callback function. May be [`None`]. + failure_callback: Option, +} + +pub struct RendererSideRouter { + weak_self: Weak, + config: MessageRouterConfig, + query_message_name: String, + cancel_message_name: String, + context_id_generator: Mutex>, + request_id_generator: Mutex>, + browser_request_info_map: Mutex>, + context_map: Mutex>>, +} + +impl RendererSideRouter { + /// Retrieve a RequestInfo object from the map based on the renderer-side + /// IDs. If |always_remove| is true then the RequestInfo object will always be + /// removed from the map. Othewise, the RequestInfo object will only be removed + /// if the query is non-persistent. + fn get_request_info( + &self, + browser_id: i32, + context_id: i32, + request_id: i32, + always_remove: bool, + ) -> Option { + let Ok(mut browser_request_info_map) = self.browser_request_info_map.lock() else { + return None; + }; + + struct Visitor { + always_remove: bool, + } + + impl BrowserInfoMapVisitor<(i32, i32), RendererSideRequestInfo> for Visitor { + fn on_next_info( + &self, + _browser_id: i32, + _key: (i32, i32), + value: &RendererSideRequestInfo, + ) -> ControlFlow { + ControlFlow::Continue(if self.always_remove || !value.is_persistent { + BrowserInfoMapVisitorResult::RemoveEntry + } else { + BrowserInfoMapVisitorResult::KeepEntry + }) + } + } + + let visitor = Visitor { always_remove }; + browser_request_info_map.find(browser_id, (context_id, request_id), Some(&visitor)) + } + + /// Returns the new request ID. + fn send_query( + &self, + browser: Option, + frame: Option, + context_id: i32, + request: V8Value, + request_info: RendererSideRequestInfo, + ) -> i32 { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let ( + Some(browser), + Some(frame), + Ok(mut request_id_generator), + Ok(mut browser_request_info_map), + ) = ( + browser, + frame, + self.request_id_generator.lock(), + self.browser_request_info_map.lock(), + ) + else { + return RESERVED_ID; + }; + let request_id = request_id_generator.next(); + let persistent = request_info.is_persistent; + browser_request_info_map.insert( + browser.identifier(), + (context_id, request_id), + request_info, + ); + + let mut message = mru::build_renderer_message( + self.config.message_size_threshold, + &self.query_message_name, + context_id, + request_id, + Some(&request), + persistent, + ); + frame.send_process_message(ProcessId::BROWSER, message.as_mut()); + + request_id + } + + /// If |request_id| is kReservedId all requests associated with |context_id| + /// will be canceled, otherwise only the specified |request_id| will be + /// canceled. Returns true if any request was canceled. + fn send_cancel( + &self, + browser: Option, + frame: Option, + context_id: i32, + request_id: i32, + ) -> bool { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let (Some(browser_id), Some(frame), Ok(mut browser_request_info_map)) = ( + browser.as_ref().map(Browser::identifier), + frame, + self.browser_request_info_map.lock(), + ) else { + return false; + }; + + let cancel_count = if request_id != RESERVED_ID { + // Cancel a single request. + if self + .get_request_info(browser_id, context_id, request_id, true) + .is_some() + { + 1 + } else { + 0 + } + } else { + // Cancel all requests with the specified context ID. + struct Visitor { + context_id: i32, + cancel_count: AtomicUsize, + } + + impl BrowserInfoMapVisitor<(i32, i32), RendererSideRequestInfo> for Visitor { + fn on_next_info( + &self, + _browser_id: i32, + key: (i32, i32), + _value: &RendererSideRequestInfo, + ) -> ControlFlow + { + ControlFlow::Continue(if self.context_id == key.0 { + self.cancel_count.fetch_add(1, Ordering::Relaxed); + BrowserInfoMapVisitorResult::RemoveEntry + } else { + BrowserInfoMapVisitorResult::KeepEntry + }) + } + } + + let visitor = Visitor { + context_id, + cancel_count: Default::default(), + }; + + browser_request_info_map.find_browser_all(browser_id, &visitor); + visitor.cancel_count.load(Ordering::Relaxed) + }; + + if cancel_count > 0 { + let mut message = + process_message_create(Some(&CefString::from(self.cancel_message_name.as_str()))); + if let Some(args) = message.as_ref().and_then(ProcessMessage::argument_list) { + args.set_int(0, context_id); + args.set_int(1, request_id); + + frame.send_process_message(ProcessId::BROWSER, message.as_mut()); + } + + true + } else { + false + } + } + + fn execute_success_callback( + &self, + browser_id: i32, + context_id: i32, + request_id: i32, + response: mru::MessagePayload, + ) { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let Some(success_callback) = self + .get_request_info(browser_id, context_id, request_id, false) + .and_then(|info| info.success_callback) + else { + return; + }; + + let Some(mut context) = self.get_context_by_id(context_id) else { + return; + }; + if context.enter() == 0 { + return; + } + + let data = match &response { + mru::MessagePayload::Empty => &[], + mru::MessagePayload::String(s) => s.as_slice().unwrap_or(&[]), + mru::MessagePayload::Binary(b) => b.data(), + }; + + #[cfg(feature = "sandbox")] + let value = v8_value_create_array_buffer_with_copy(data.as_ptr() as *mut u8, data.len()); + #[cfg(not(feature = "sandbox"))] + let value = v8_value_create_array_buffer( + data.as_ptr() as *mut u8, + data.len(), + Some(&mut mru::BinaryValueArrayBufferReleaseCallback::new( + response, + )), + ); + + context.exit(); + + success_callback.execute_function_with_context(Some(&mut context), None, Some(&[value])); + } + + fn execute_failure_callback( + &self, + browser_id: i32, + context_id: i32, + request_id: i32, + error_code: i32, + error_message: &str, + ) { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let Some(failure_callback) = self + .get_request_info(browser_id, context_id, request_id, true) + .and_then(|info| info.failure_callback) + else { + return; + }; + + let Some(mut context) = self.get_context_by_id(context_id) else { + return; + }; + + failure_callback.execute_function_with_context( + Some(&mut context), + None, + Some(&[ + v8_value_create_int(error_code), + v8_value_create_string(Some(&CefString::from(error_message))), + ]), + ); + } + + fn create_id_for_context(&self, context: V8Context) -> i32 { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + + // The context should not already have an associated ID. + debug_assert_eq!(self.get_id_for_context(context.clone(), false), RESERVED_ID); + + let (Ok(mut context_id_generator), Ok(mut context_map)) = + (self.context_id_generator.lock(), self.context_map.lock()) + else { + return RESERVED_ID; + }; + let context_id = context_id_generator.next(); + context_map.insert(context_id, Some(context)); + context_id + } + + fn get_id_for_context(&self, mut context: V8Context, remove: bool) -> i32 { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let Ok(mut context_map) = self.context_map.lock() else { + return RESERVED_ID; + }; + let context_id = context_map + .iter() + .find(|(_, entry)| { + entry + .as_ref() + .map_or(0, |entry| entry.is_same(Some(&mut context))) + != 0 + }) + .map(|(context_id, _)| *context_id); + + if let Some(context_id) = context_id { + if remove { + context_map.remove(&context_id); + } + context_id + } else { + RESERVED_ID + } + } + + fn get_context_by_id(&self, context_id: i32) -> Option { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let context_map = self.context_map.lock().ok()?; + context_map.get(&context_id)?.clone() + } +} + +impl MessageRouterRendererSide for RendererSideRouter { + /// Create a new router with the specified configuration. + fn new(config: MessageRouterConfig) -> Arc { + let query_message_name = format!("{}{MESSAGE_SUFFIX}", config.js_query_function); + let cancel_message_name = format!("{}{MESSAGE_SUFFIX}", config.js_cancel_function); + Arc::new_cyclic(|weak_self| Self { + weak_self: weak_self.clone(), + config, + query_message_name, + cancel_message_name, + context_id_generator: Mutex::new(IdGenerator::new(i32::MAX)), + request_id_generator: Mutex::new(IdGenerator::new(i32::MAX)), + browser_request_info_map: Default::default(), + context_map: Default::default(), + }) + } + + /// Returns the number of queries currently pending for the specified + /// |browser| and/or |context|. Either or both values may be empty. + fn pending_count(&self, browser: Option, context: Option) -> usize { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let Ok(mut browser_request_info_map) = self.browser_request_info_map.lock() else { + return 0; + }; + if browser_request_info_map.is_empty() { + return 0; + } + + if let Some(context) = context { + let context_id = self.get_id_for_context(context, false); + if context_id == RESERVED_ID { + // Nothing associated with the specified context. + return 0; + } + + // Need to iterate over each RequestInfo object to test the context. + struct Visitor { + context_id: i32, + count: AtomicUsize, + } + + impl BrowserInfoMapVisitor<(i32, i32), RendererSideRequestInfo> for Visitor { + fn on_next_info( + &self, + _browser_id: i32, + key: (i32, i32), + _value: &RendererSideRequestInfo, + ) -> ControlFlow + { + if key.0 == self.context_id { + self.count.fetch_add(1, Ordering::Relaxed); + } + ControlFlow::Continue(BrowserInfoMapVisitorResult::KeepEntry) + } + } + + let visitor = Visitor { + context_id, + count: Default::default(), + }; + + if let Some(browser) = browser { + browser_request_info_map.find_browser_all(browser.identifier(), &visitor); + } else { + browser_request_info_map.find_all(&visitor); + } + + visitor.count.load(Ordering::Relaxed) + } else if let Some(browser) = browser { + browser_request_info_map.browser_len(browser.identifier()) + } else { + browser_request_info_map.len() + } + } +} + +impl MessageRouterRendererSideHandlerCallbacks for RendererSideRouter { + /// Call from CefRenderProcessHandler::OnContextCreated. Registers the + /// JavaScripts functions with the new context. + fn on_context_created( + &self, + _browser: Option, + _frame: Option, + context: Option, + ) { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + + // Register function handlers with the 'window' object. + let Some(window) = context.and_then(|context| context.global()) else { + return; + }; + + let mut handler = RendererSideV8Handler::new( + self.weak_self.clone(), + self.config.clone(), + Default::default(), + ); + let attributes = sys::cef_v8_propertyattribute_t( + [ + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_READONLY, + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTENUM, + sys::cef_v8_propertyattribute_t::V8_PROPERTY_ATTRIBUTE_DONTDELETE, + ] + .into_iter() + .fold(0, |acc, attr| acc | attr.0), + ) + .into(); + + // Add the query function. + let name = CefString::from(self.config.js_query_function.as_str()); + let mut query_func = v8_value_create_function(Some(&name), Some(&mut handler)); + window.set_value_bykey(Some(&name), query_func.as_mut(), attributes); + + // Add the cancel function. + let name = CefString::from(self.config.js_cancel_function.as_str()); + let mut cancel_func = v8_value_create_function(Some(&name), Some(&mut handler)); + window.set_value_bykey(Some(&name), cancel_func.as_mut(), attributes); + } + + /// Call from CefRenderProcessHandler::OnContextReleased. Any pending queries + /// associated with the released context will be canceled and + /// Handler::OnQueryCanceled will be called in the browser process. + fn on_context_released( + &self, + browser: Option, + frame: Option, + context: Option, + ) { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + + // Get the context ID and remove the context from the map. + let context_id = context + .map(|context| self.get_id_for_context(context, true)) + .unwrap_or(RESERVED_ID); + if context_id != RESERVED_ID { + // Cancel all pending requests for the context. + self.send_cancel(browser, frame, context_id, RESERVED_ID); + } + } + + /// Call from CefRenderProcessHandler::OnProcessMessageReceived. Returns true + /// if the message is handled by this router or false otherwise. + fn on_process_message_received( + &self, + browser: Option, + _frame: Option, + _source_process: Option, + message: Option, + ) -> bool { + debug_assert_ne!(currently_on(ThreadId::RENDERER), 0); + let (Some(browser), Some(message)) = (browser, message) else { + return true; + }; + + let name = CefString::from(&message.name()).to_string(); + if name != self.query_message_name { + return false; + } + + let content = mru::BrowserMessage::from(Some(message)); + if content.is_success { + self.execute_success_callback( + browser.identifier(), + content.context_id, + content.request_id, + content.payload, + ); + } else { + let error_message = match content.payload { + mru::MessagePayload::String(s) => s.to_string(), + _ => Default::default(), + }; + self.execute_failure_callback( + browser.identifier(), + content.context_id, + content.request_id, + content.error_code, + error_message.as_str(), + ); + } + + true + } +} + +wrap_v8_handler! { + struct RendererSideV8Handler { + router: Weak, + config: MessageRouterConfig, + context_id: OnceLock, + } + + impl V8Handler { + fn execute( + &self, + name: Option<&CefString>, + _object: Option<&mut V8Value>, + arguments: Option<&[Option]>, + retval: Option<&mut Option>, + exception: Option<&mut CefString>, + ) -> i32 { + macro_rules! return_exception { + ($message:expr) => { + if let Some(exception) = exception { + *exception = CefString::from($message); + } + + return 1; + }; + } + + let Some(name) = name else { + return_exception!("Missing function name"); + }; + let name = name.to_string(); + if name == self.config.js_query_function { + let Some(arg) = arguments + .filter(|arguments| arguments.len() == 1) + .and_then(|arguments| arguments[0].as_ref()) + .filter(|arg| arg.is_object() != 0) + else { + return_exception!("Invalid arguments; expecting a single object"); + }; + + let key = CefString::from(ObjectMember::REQUEST); + let Some(request) = arg.value_bykey(Some(&key)) else { + return_exception!(format!( + "Invalid arguments; object member '{}' is required", + ObjectMember::REQUEST + ) + .as_str()); + }; + if request.is_string() == 0 && request.is_array_buffer() == 0 { + return_exception!(format!("Invalid arguments; object member '{}' must have type string or ArrayBuffer", ObjectMember::REQUEST).as_str()); + } + + let key = CefString::from(ObjectMember::ON_SUCCESS); + let success = if let Some(success) = arg.value_bykey(Some(&key)) { + if success.is_function() == 0 { + return_exception!(format!( + "Invalid arguments; object member '{}' must have type function", + ObjectMember::ON_SUCCESS + ) + .as_str()); + } + Some(success) + } else { + None + }; + + let key = CefString::from(ObjectMember::ON_FAILURE); + let failure = if let Some(failure) = arg.value_bykey(Some(&key)) { + if failure.is_function() == 0 { + return_exception!(format!( + "Invalid arguments; object member '{}' must have type function", + ObjectMember::ON_FAILURE + ) + .as_str()); + } + Some(failure) + } else { + None + }; + + let key = CefString::from(ObjectMember::PERSISTENT); + let persistent = if let Some(persistent) = arg.value_bykey(Some(&key)) { + if persistent.is_bool() == 0 { + return_exception!(format!( + "Invalid arguments; object member '{}' must have type boolean", + ObjectMember::PERSISTENT + ) + .as_str()); + } + Some(persistent) + } else { + None + }; + + if let (Some(router), Some(context)) = + (self.router.upgrade(), v8_context_get_current_context()) + { + let context_id = self.get_id_for_context(context.clone()); + let persistent = persistent.map_or(0, |value| value.bool_value()) != 0; + let request_id = router.send_query( + context.browser(), + context.frame(), + context_id, + request, + RendererSideRequestInfo { + is_persistent: persistent, + success_callback: success, + failure_callback: failure, + }, + ); + + if let Some(retval) = retval { + *retval = v8_value_create_int(request_id); + } + return 1; + } + } else if name == self.config.js_cancel_function { + let Some(arg) = arguments + .filter(|arguments| arguments.len() == 1) + .and_then(|arguments| arguments[0].as_ref()) + .filter(|arg| arg.is_int() != 0) + else { + return_exception!("Invalid arguments; expecting a single integer"); + }; + + let request_id = arg.int_value(); + if request_id != RESERVED_ID { + if let (Some(router), Some(context)) = + (self.router.upgrade(), v8_context_get_current_context()) + { + let context_id = self.get_id_for_context(context.clone()); + let result = router.send_cancel( + context.browser(), + context.frame(), + context_id, + request_id, + ); + + if let Some(retval) = retval { + *retval = v8_value_create_bool(result.into()); + } + return 1; + } + } + } + + 0 + } + } +} + +impl RendererSideV8Handler { + fn get_id_for_context(&self, context: V8Context) -> i32 { + *self.context_id.get_or_init(|| { + let Some(router) = self.router.upgrade() else { + return RESERVED_ID; + }; + router.create_id_for_context(context) + }) + } +} diff --git a/cef/src/wrapper/message_router_utils.rs b/cef/src/wrapper/message_router_utils.rs new file mode 100644 index 0000000..369c846 --- /dev/null +++ b/cef/src/wrapper/message_router_utils.rs @@ -0,0 +1,726 @@ +use super::message_router::*; +use crate::*; +use std::{ + io::{self, Cursor, Read, Write}, + marker::PhantomData, + mem, + rc::Rc, + slice, + sync::Arc, +}; + +const NO_ERROR: i32 = 0; + +const CONTEXT_ID: usize = 0; +const REQUEST_ID: usize = 1; +const RENDERER_PAYLOAD: usize = 2; +const IS_SUCCESS: usize = 2; +const BROWSER_PAYLOAD: usize = 3; +const IS_PERSISTENT: usize = 3; + +pub trait ProcessMessageBuilder { + fn build_browser_response(&self, context_id: i32, request_id: i32) -> Option; + fn build_renderer_message( + &self, + context_id: i32, + request_id: i32, + persistent: bool, + ) -> Option; +} + +#[derive(Clone, Default)] +pub enum MessagePayload { + #[default] + Empty, + String(CefStringUtf8), + Binary(Arc), +} + +impl MessagePayload { + fn size(&self) -> usize { + match self { + Self::Empty => 0, + Self::String(s) => s.as_slice().map(|s| s.len()).unwrap_or(0), + Self::Binary(b) => b.data().len(), + } + } + + fn read_string(f: &mut R) -> io::Result { + let mut buffer = vec![]; + f.read_to_end(&mut buffer)?; + Ok(String::from_utf8(buffer) + .map(|value| value.as_str().into()) + .unwrap_or(Self::Empty)) + } + + // fn read_binary(f: &mut R) -> io::Result { + // let mut buffer = vec![]; + // f.read_to_end(&mut buffer)?; + // Ok(binary_value_create(Some(&buffer)) + // .map(|value| Self::Binary(Arc::new(BinaryValueBuffer::new(None, Some(value))))) + // .unwrap_or(Self::Empty)) + // } + + fn write(&self, f: &mut W) -> io::Result<()> { + let buffer = match self { + Self::String(s) => s.as_slice(), + Self::Binary(b) if !b.data().is_empty() => Some(b.data()), + _ => None, + }; + if let Some(buffer) = buffer { + f.write_all(buffer)?; + } + Ok(()) + } +} + +impl From<&CefString> for MessagePayload { + fn from(s: &CefString) -> Self { + Self::String(CefStringUtf8::from(s)) + } +} + +impl From<&str> for MessagePayload { + fn from(s: &str) -> Self { + Self::String(CefStringUtf8::from(s)) + } +} + +impl From> for MessagePayload { + fn from(v: Option<&V8Value>) -> Self { + let b = v.and_then(|v| { + let ptr = v.array_buffer_data(); + let size = v.array_buffer_byte_length(); + if ptr.is_null() || size == 0 { + return None; + } + + let data = unsafe { slice::from_raw_parts(ptr as *const u8, size) }; + binary_value_create(Some(data)) + }); + b.map(|value| Self::Binary(Arc::new(BinaryValueBuffer::new(None, Some(value))))) + .unwrap_or(Self::Empty) + } +} + +impl From<&[u8]> for MessagePayload { + fn from(data: &[u8]) -> Self { + if data.is_empty() { + Self::Empty + } else { + Self::Binary(Arc::new(BinaryValueBuffer::new( + None, + binary_value_create(Some(data)), + ))) + } + } +} + +#[derive(Clone, Default)] +pub struct BrowserMessage { + pub context_id: i32, + pub request_id: i32, + pub is_success: bool, + pub error_code: i32, + pub payload: MessagePayload, +} + +impl From> for BrowserMessage { + fn from(message: Option) -> Self { + let Some(message) = message else { + return Default::default(); + }; + + if let Some(args) = message.argument_list() { + let context_id = args.int(CONTEXT_ID); + let request_id = args.int(REQUEST_ID); + let is_success = args.bool(IS_SUCCESS); + + if is_success != 0 { + debug_assert_eq!(args.size(), 4); + match args.get_type(BROWSER_PAYLOAD) { + ValueType::STRING => Self { + context_id, + request_id, + is_success: true, + error_code: NO_ERROR, + payload: MessagePayload::String(CefStringUtf8::from(&CefString::from( + &args.string(BROWSER_PAYLOAD), + ))), + }, + ValueType::BINARY => Self { + context_id, + request_id, + is_success: true, + error_code: NO_ERROR, + payload: MessagePayload::Binary(Arc::new(BinaryValueBuffer::new( + Some(message), + args.binary(BROWSER_PAYLOAD), + ))), + }, + payload_type => { + assert_eq!(payload_type, ValueType::NULL); + Self { + context_id, + request_id, + is_success: true, + error_code: NO_ERROR, + payload: MessagePayload::Binary(Arc::new(EmptyBinaryBuffer)), + } + } + } + } else { + debug_assert_eq!(args.size(), 5); + Self { + context_id, + request_id, + is_success: false, + error_code: args.int(3), + payload: MessagePayload::String(CefStringUtf8::from(&CefString::from( + &args.string(4), + ))), + } + } + } else if let Some(region) = message + .shared_memory_region() + .filter(|region| region.is_valid() != 0) + { + debug_assert!(region.size() >= BrowserMessageHeader::SIZE); + debug_assert!(!region.memory().is_null()); + let buffer = + unsafe { slice::from_raw_parts(region.memory() as *const u8, region.size()) }; + let header = { + let mut cursor = Cursor::new(&buffer[..BrowserMessageHeader::SIZE]); + BrowserMessageHeader::read(&mut cursor).unwrap_or_default() + }; + if header.is_binary { + Self { + context_id: header.context_id, + request_id: header.request_id, + is_success: true, + error_code: NO_ERROR, + payload: MessagePayload::Binary(Arc::new(SharedMemoryRegionBuffer::new( + Some(region), + BrowserMessageHeader::SIZE, + ))), + } + } else { + let mut cursor = Cursor::new(&buffer[BrowserMessageHeader::SIZE..]); + Self { + context_id: header.context_id, + request_id: header.request_id, + is_success: true, + error_code: NO_ERROR, + payload: MessagePayload::read_string(&mut cursor).unwrap_or_default(), + } + } + } else { + Default::default() + } + } +} + +#[derive(Clone, Default)] +pub struct RenderMessage { + pub context_id: i32, + pub request_id: i32, + pub is_persistent: bool, + pub payload: MessagePayload, +} + +impl From> for RenderMessage { + fn from(message: Option) -> Self { + let Some(message) = message else { + return Default::default(); + }; + + if let Some(args) = message.argument_list() { + debug_assert_eq!(args.size(), 4); + let context_id = args.int(CONTEXT_ID); + let request_id = args.int(REQUEST_ID); + let is_persistent = args.bool(IS_PERSISTENT) != 0; + + match args.get_type(RENDERER_PAYLOAD) { + ValueType::STRING => Self { + context_id, + request_id, + is_persistent, + payload: MessagePayload::String(CefStringUtf8::from(&CefString::from( + &args.string(RENDERER_PAYLOAD), + ))), + }, + ValueType::BINARY => Self { + context_id, + request_id, + is_persistent, + payload: MessagePayload::Binary(Arc::new(BinaryValueBuffer::new( + Some(message), + args.binary(RENDERER_PAYLOAD), + ))), + }, + payload_type => { + debug_assert_eq!(payload_type, ValueType::NULL); + Self { + context_id, + request_id, + is_persistent, + payload: MessagePayload::Binary(Arc::new(EmptyBinaryBuffer)), + } + } + } + } else if let Some(region) = message + .shared_memory_region() + .filter(|region| region.is_valid() != 0) + { + debug_assert!(region.size() >= RendererMessageHeader::::SIZE); + debug_assert!(!region.memory().is_null()); + let buffer = + unsafe { slice::from_raw_parts(region.memory() as *const u8, region.size()) }; + let header = { + let mut cursor = Cursor::new(&buffer[..RendererMessageHeader::::SIZE]); + RendererMessageHeader::::read(&mut cursor).unwrap_or_default() + }; + if header.is_binary { + Self { + context_id: header.context_id, + request_id: header.request_id, + is_persistent: header.is_persistent, + payload: MessagePayload::Binary(Arc::new(SharedMemoryRegionBuffer::new( + Some(region), + RendererMessageHeader::::SIZE, + ))), + } + } else { + let mut cursor = Cursor::new(&buffer[RendererMessageHeader::::SIZE..]); + Self { + context_id: header.context_id, + request_id: header.request_id, + is_persistent: header.is_persistent, + payload: MessagePayload::read_string(&mut cursor).unwrap_or_default(), + } + } + } else { + Default::default() + } + } +} + +#[cfg(not(feature = "sandbox"))] +wrap_v8_array_buffer_release_callback! { + pub struct BinaryValueArrayBufferReleaseCallback { + value: MessagePayload, + } + + impl V8ArrayBufferReleaseCallback { + fn release_buffer(&self, _buffer: *mut u8) {} + } +} + +trait MessageHeader: Sized { + const SIZE: usize; + + fn new(context_id: i32, request_id: i32, is_binary: bool) -> Self; + fn read(f: &mut R) -> io::Result; + fn write(&self, f: &mut W) -> io::Result<()>; +} + +#[derive(Clone, Default)] +struct BrowserMessageHeader { + context_id: i32, + request_id: i32, + is_binary: bool, +} + +impl MessageHeader for BrowserMessageHeader { + const SIZE: usize = mem::size_of::() * 2 + 1; + + fn new(context_id: i32, request_id: i32, is_binary: bool) -> Self { + Self { + context_id, + request_id, + is_binary, + } + } + + fn read(f: &mut R) -> io::Result { + let mut data = [0_u8; mem::size_of::()]; + f.read_exact(&mut data)?; + let context_id = i32::from_ne_bytes(data); + f.read_exact(&mut data)?; + let request_id = i32::from_ne_bytes(data); + let mut flags = [0_u8]; + f.read_exact(&mut flags)?; + let is_binary = flags[0] != 0; + + Ok(Self { + context_id, + request_id, + is_binary, + }) + } + + fn write(&self, f: &mut W) -> io::Result<()> { + f.write_all(&self.context_id.to_ne_bytes())?; + f.write_all(&self.request_id.to_ne_bytes())?; + f.write_all(&[self.is_binary.into()]) + } +} + +#[derive(Clone, Default)] +struct RendererMessageHeader { + context_id: i32, + request_id: i32, + is_persistent: bool, + is_binary: bool, +} + +impl MessageHeader for RendererMessageHeader { + const SIZE: usize = mem::size_of::() * 2 + 2; + + fn new(context_id: i32, request_id: i32, is_binary: bool) -> Self { + Self { + context_id, + request_id, + is_persistent: DEFAULT_PERSISTENT, + is_binary, + } + } + + fn read(f: &mut R) -> io::Result { + let mut data = [0_u8; mem::size_of::()]; + f.read_exact(&mut data)?; + let context_id = i32::from_ne_bytes(data); + f.read_exact(&mut data)?; + let request_id = i32::from_ne_bytes(data); + let mut flags = [0_u8; 2]; + f.read_exact(&mut flags)?; + let is_persistent = flags[0] != 0; + let is_binary = flags[1] != 0; + + Ok(Self { + context_id, + request_id, + is_persistent, + is_binary, + }) + } + + fn write(&self, f: &mut W) -> io::Result<()> { + f.write_all(&self.context_id.to_ne_bytes())?; + f.write_all(&self.request_id.to_ne_bytes())?; + f.write_all(&[self.is_persistent.into()])?; + f.write_all(&[self.is_binary.into()]) + } +} + +fn build_browser_list_message( + name: &str, + context_id: i32, + request_id: i32, + payload: MessagePayload, +) -> Option { + let message = process_message_create(Some(&CefString::from(name)))?; + let args = message.argument_list()?; + args.set_int(CONTEXT_ID, context_id); + args.set_int(REQUEST_ID, request_id); + args.set_bool(IS_SUCCESS, 1); + + match payload { + MessagePayload::Empty => args.set_null(BROWSER_PAYLOAD), + MessagePayload::String(value) => { + args.set_string(BROWSER_PAYLOAD, Some(&CefString::from(&value))) + } + MessagePayload::Binary(value) => args.set_binary( + BROWSER_PAYLOAD, + binary_value_create(Some(value.data())).as_mut(), + ), + }; + + Some(message) +} + +fn build_render_list_message( + name: &str, + context_id: i32, + request_id: i32, + payload: MessagePayload, + is_persistent: bool, +) -> Option { + let message = process_message_create(Some(&CefString::from(name)))?; + let args = message.argument_list()?; + args.set_int(CONTEXT_ID, context_id); + args.set_int(REQUEST_ID, request_id); + + match payload { + MessagePayload::Empty => args.set_null(RENDERER_PAYLOAD), + MessagePayload::String(value) => { + args.set_string(RENDERER_PAYLOAD, Some(&CefString::from(&value))) + } + MessagePayload::Binary(value) => args.set_binary( + RENDERER_PAYLOAD, + binary_value_create(Some(value.data())).as_mut(), + ), + }; + + args.set_bool(IS_PERSISTENT, is_persistent.into()); + + Some(message) +} + +struct MessagePayloadBuilder { + pub name: String, + pub payload: MessagePayload, +} + +impl ProcessMessageBuilder for MessagePayloadBuilder { + fn build_browser_response(&self, context_id: i32, request_id: i32) -> Option { + build_browser_list_message(&self.name, context_id, request_id, self.payload.clone()) + } + + fn build_renderer_message( + &self, + context_id: i32, + request_id: i32, + persistent: bool, + ) -> Option { + build_render_list_message( + &self.name, + context_id, + request_id, + self.payload.clone(), + persistent, + ) + } +} + +enum SharedProcessMessageRouter
+where + Header: MessageHeader, +{ + SharedMemory { + builder: SharedProcessMessageBuilder, + is_binary: bool, + _phantom: PhantomData
, + }, + Payload(MessagePayloadBuilder), +} + +impl
SharedProcessMessageRouter
+where + Header: MessageHeader, +{ + fn new(name: &str, payload: MessagePayload) -> Self { + let message_size = Header::SIZE + payload.size(); + let builder = + match shared_process_message_builder_create(Some(&CefString::from(name)), message_size) + { + Some(builder) if builder.is_valid() != 0 => builder, + _ => { + return Self::Payload(MessagePayloadBuilder { + name: name.to_owned(), + payload, + }) + } + }; + + let buffer = + unsafe { slice::from_raw_parts_mut(builder.memory() as *mut u8, message_size) }; + let mut cursor = Cursor::new(&mut buffer[Header::SIZE..]); + if payload.write(&mut cursor).is_err() { + return Self::Payload(MessagePayloadBuilder { + name: name.to_owned(), + payload, + }); + } + + Self::SharedMemory { + builder, + is_binary: matches!(payload, MessagePayload::Binary(_)), + _phantom: PhantomData, + } + } +} + +impl
ProcessMessageBuilder for SharedProcessMessageRouter
+where + Header: MessageHeader, +{ + fn build_browser_response(&self, context_id: i32, request_id: i32) -> Option { + match self { + Self::SharedMemory { + builder, is_binary, .. + } => { + let buffer = + unsafe { slice::from_raw_parts_mut(builder.memory() as *mut u8, Header::SIZE) }; + let mut cursor = Cursor::new(buffer); + Header::new(context_id, request_id, *is_binary) + .write(&mut cursor) + .ok()?; + + builder.build() + } + Self::Payload(builder) => builder.build_browser_response(context_id, request_id), + } + } + + fn build_renderer_message( + &self, + context_id: i32, + request_id: i32, + persistent: bool, + ) -> Option { + match self { + Self::SharedMemory { + builder, is_binary, .. + } => { + let buffer = + unsafe { slice::from_raw_parts_mut(builder.memory() as *mut u8, Header::SIZE) }; + let mut cursor = Cursor::new(buffer); + Header::new(context_id, request_id, *is_binary) + .write(&mut cursor) + .ok()?; + + builder.build() + } + Self::Payload(builder) => { + builder.build_renderer_message(context_id, request_id, persistent) + } + } + } +} + +pub struct EmptyBinaryBuffer; + +impl BinaryBuffer for EmptyBinaryBuffer { + fn data(&self) -> &[u8] { + &[] + } + + fn data_mut(&mut self) -> &mut [u8] { + &mut [] + } +} + +pub struct BinaryValueBuffer { + _message: Option, + value: Option, +} + +impl BinaryValueBuffer { + pub fn new(message: Option, value: Option) -> Self { + Self { + _message: message, + value, + } + } +} + +impl BinaryBuffer for BinaryValueBuffer { + fn data(&self) -> &[u8] { + self.value.as_ref().map_or(&[], |v| unsafe { + slice::from_raw_parts(v.raw_data() as *const u8, v.size()) + }) + } + + fn data_mut(&mut self) -> &mut [u8] { + self.value.as_mut().map_or(&mut [], |v| unsafe { + slice::from_raw_parts_mut(v.raw_data() as *mut u8, v.size()) + }) + } +} + +struct SharedMemoryRegionBuffer { + region: Option, + offset: usize, +} + +impl SharedMemoryRegionBuffer { + fn new(region: Option, offset: usize) -> Self { + Self { region, offset } + } + + fn data(&self) -> *mut u8 { + self.region + .as_ref() + .map_or(std::ptr::null_mut(), |r| unsafe { + r.memory().add(self.offset) as *mut u8 + }) + } + + fn size(&self) -> usize { + self.region + .as_ref() + .map_or(0, |r| r.size().saturating_sub(self.offset)) + } +} + +impl BinaryBuffer for SharedMemoryRegionBuffer { + fn data(&self) -> &[u8] { + let data = self.data(); + let size = self.size(); + if data.is_null() || size == 0 { + &[] + } else { + unsafe { slice::from_raw_parts(data as *const u8, size) } + } + } + + fn data_mut(&mut self) -> &mut [u8] { + let data = self.data(); + let size = self.size(); + if data.is_null() || size == 0 { + &mut [] + } else { + unsafe { slice::from_raw_parts_mut(data, size) } + } + } +} + +pub fn create_browser_response_builder( + threshold: usize, + name: &str, + payload: MessagePayload, +) -> Rc { + if payload.size() < threshold { + Rc::new(MessagePayloadBuilder { + name: name.to_string(), + payload, + }) + } else { + Rc::new(SharedProcessMessageRouter::::new( + name, payload, + )) + } +} + +pub fn build_renderer_message( + threshold: usize, + name: &str, + context_id: i32, + request_id: i32, + request: Option<&V8Value>, + persistent: bool, +) -> Option { + let payload = request + .map(|request| { + if request.is_string() != 0 { + MessagePayload::from(&CefString::from(&request.string_value())) + } else { + MessagePayload::from(Some(request)) + } + }) + .unwrap_or(MessagePayload::Empty); + + let builder: Box = if payload.size() < threshold { + Box::new(MessagePayloadBuilder { + name: name.to_string(), + payload, + }) + } else if persistent { + Box::new(SharedProcessMessageRouter::>::new(name, payload)) + } else { + Box::new(SharedProcessMessageRouter::>::new(name, payload)) + }; + + builder.build_renderer_message(context_id, request_id, persistent) +} diff --git a/cef/src/wrapper/mod.rs b/cef/src/wrapper/mod.rs new file mode 100644 index 0000000..893a9aa --- /dev/null +++ b/cef/src/wrapper/mod.rs @@ -0,0 +1,8 @@ +pub mod browser_info_map; +pub mod byte_read_handler; +pub mod message_router; +pub mod resource_manager; +pub mod stream_resource_handler; +pub mod zip_archive; + +mod message_router_utils; diff --git a/cef/src/wrapper/resource_manager.rs b/cef/src/wrapper/resource_manager.rs new file mode 100644 index 0000000..e5ccb99 --- /dev/null +++ b/cef/src/wrapper/resource_manager.rs @@ -0,0 +1,1155 @@ +use super::{stream_resource_handler::StreamResourceHandler, zip_archive::*}; +use crate::*; +use std::{ + collections::{BTreeMap, VecDeque}, + path::PathBuf, + sync::{Arc, Mutex, Weak}, +}; + +pub type UrlFilter = Box String>; +pub type MimeTypeResolver = Box String>; + +struct RequestParams { + url: String, + browser: Browser, + frame: Frame, + request: Request, + url_filter: Arc, + mime_type_resolver: Arc, +} + +#[derive(Default)] +struct ProviderEntry { + provider: Option>, + order: i32, + identifier: String, + pending_requests: VecDeque>>, + deletion_pending: bool, +} + +/// Values associated with the pending request only. Ownership will be passed +/// between requests and the resource manager as request handling proceeds. +struct RequestState { + manager: Weak>, + + /// Callback to execute once request handling is complete. + callback: Option, + + /// Position of the currently associated [ProviderEntry] in the `ResourceManagerProviders` + /// list. + current_entry: usize, + + /// Position of this request object in the currently associated + /// [ProviderEntry]'s `pending_requests` list. + current_request: usize, + + /// Params that will be copied to each request object. + params: Arc, +} + +impl Drop for RequestState { + fn drop(&mut self) { + // Always execute the callback. + if let Some(callback) = self.callback.take() { + callback.cont(); + } + } +} + +pub struct ResourceManagerRequest { + weak_self: Weak>, + state: Option, + params: Arc, +} + +impl ResourceManagerRequest { + fn new(state: Option, params: Arc) -> Arc> { + Arc::new_cyclic(|weak_self| { + Mutex::new(Self { + weak_self: weak_self.clone(), + state, + params, + }) + }) + } + + /// Returns the URL associated with this request. The returned value will be fully qualified + /// but will not contain query or fragment components. It will already have been passed through + /// the URL filter. + pub fn url(&self) -> &str { + &self.params.url + } + + /// Returns the [Browser] associated with this request. + pub fn browser(&self) -> &Browser { + &self.params.browser + } + + /// Returns the [Frame] associated with this request. + pub fn frame(&self) -> &Frame { + &self.params.frame + } + + /// Returns the [Request] associated with this request. + pub fn request(&self) -> &Request { + &self.params.request + } + + /// Returns the current URL filter. + pub fn url_filter(&self) -> &UrlFilter { + &self.params.url_filter + } + + /// Returns the current mime type resolver. + pub fn mime_type_resolver(&self) -> &MimeTypeResolver { + &self.params.mime_type_resolver + } + + pub fn continue_request(&mut self, handler: Option) { + // Disassociate `self.state` immediately so that [ResourceManagerProvider::on_request_canceled] + // is not called unexpectedly if [ResourceManagerProvider::on_request] calls this method + // and then calls [ResourceManager::remove]. + let Some(state) = self.state.take() else { + return; + }; + + let io_thread_id = ThreadId::IO; + let mut task = ContinueRequest::new(Arc::new(Mutex::new(Some(state))), handler); + post_task(io_thread_id, Some(&mut task)); + } + + pub fn stop_request(&mut self) { + // Disassociate `self.state` immediately so that [ResourceManagerProvider::on_request_canceled] + // is not called unexpectedly if [ResourceManagerProvider::on_request] calls this method + // and then calls [ResourceManager::remove]. + let Some(state) = self.state.take() else { + return; + }; + + let io_thread_id = ThreadId::IO; + let mut task = StopRequest::new(Arc::new(Mutex::new(Some(state)))); + post_task(io_thread_id, Some(&mut task)); + } + + /// Detaches and returns `self.state` if the provider indicates that it will not handle the + /// request. Note that `self.state` may already be [None] if [ResourceManagerProvider::on_request] + /// executes a callback before returning, in which case execution will continue asynchronously + /// in any case. + fn send_request(&mut self) -> Option { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "send_request must be called on the IO thread" + ); + + let state = self.state.as_ref()?; + let Some(manager) = state.manager.upgrade() else { + return self.state.take(); + }; + let Ok(mut manager) = manager.lock() else { + return self.state.take(); + }; + let Some(provider_entry) = manager.providers.get_mut(state.current_entry) else { + return self.state.take(); + }; + let Some(provider) = provider_entry.provider.as_mut() else { + return self.state.take(); + }; + + let Some(request) = self.weak_self.upgrade() else { + return self.state.take(); + }; + if !provider.on_request(request) { + return self.state.take(); + } + + None + } +} + +wrap_task! { + struct ContinueRequest { + state: Arc>>, + handler: Option, + } + + impl Task { + fn execute(&self) { + let Ok(mut state) = self.state.lock() else { + return; + }; + let Some(state) = state.take() else { + return; + }; + let Some(manager) = state.manager.upgrade() else { + return; + }; + let Ok(mut manager) = manager.lock() else { + return; + }; + + manager.continue_request(state, self.handler.clone()); + } + } +} + +wrap_task! { + struct StopRequest { + state: Arc>>, + } + + impl Task { + fn execute(&self) { + let Ok(mut state) = self.state.lock() else { + return; + }; + let Some(state) = state.take() else { + return; + }; + let Some(manager) = state.manager.upgrade() else { + return; + }; + let Ok(mut manager) = manager.lock() else { + return; + }; + + manager.stop_request(state); + } + } +} + +/// Interface implemented by resource [ResourceManagerProviders]. A [ResourceManagerProvider] may +/// be created on any thread but the methods will be called on, and the object will be destroyed +/// on, the browser process IO thread. +pub trait ResourceManagerProvider: Send { + /// Called to handle a request. If the ResourceManagerProvider knows immediately that it will + /// not handle the request return false. Otherwise, return true and call [ResourceManagerRequest::continue_request] + /// or [ResourceManagerRequest::stop_request] either in this method or asynchronously to + /// indicate completion. See comments on [ResourceManagerRequest] for additional usage + /// information. + fn on_request(&self, request: Arc>) -> bool; + + /// Called when a request has been canceled. It is still safe to dereference `request` but any + /// calls to [ResourceManagerRequest::continue_request] or [ResourceManagerRequest::stop_request] + /// will be ignored. + fn on_request_canceled(&self, _request: Arc>) {} +} + +/// Provider of fixed contents. +struct ContentProvider { + url: String, + content: String, + mime_type: String, +} + +impl ContentProvider { + fn new_resource_manager_provider( + url: &str, + content: &str, + mime_type: &str, + ) -> Box { + Box::new(Self { + url: url.to_string(), + content: content.to_string(), + mime_type: mime_type.to_string(), + }) + } +} + +impl ResourceManagerProvider for ContentProvider { + fn on_request(&self, request: Arc>) -> bool { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "on_request must be called on the IO thread" + ); + + let Ok(mut request) = request.lock() else { + return false; + }; + + if request.url() != self.url { + // Not handled by this provider. + return false; + } + + let mut data: Vec<_> = self.content.bytes().collect(); + let Some(stream) = stream_reader_create_for_data(data.as_mut_ptr(), data.len()) else { + return false; + }; + + let mime_type = if self.mime_type.is_empty() { + self.mime_type.clone() + } else { + (request.mime_type_resolver())(&self.url) + }; + + request.continue_request(Some(StreamResourceHandler::new_with_stream( + mime_type, stream, + ))); + true + } +} + +/// Provider of contents loaded from a directory on the file system. +struct DirectoryProvider { + url_path: String, + directory_path: PathBuf, +} + +impl DirectoryProvider { + fn new_resource_manager_provider( + url_path: &str, + directory_path: PathBuf, + ) -> Box { + Box::new(Self { + url_path: normalize_url_path(url_path), + directory_path, + }) + } +} + +impl ResourceManagerProvider for DirectoryProvider { + fn on_request(&self, request: Arc>) -> bool { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "on_request must be called on the IO thread" + ); + + let Some(file_path) = ({ + let Ok(request) = request.lock() else { + return false; + }; + let url = request.url(); + if !url.starts_with(self.url_path.as_str()) { + // Not handled by this provider. + return false; + } + + get_file_relative_path(self.directory_path.clone(), self.url_path.as_str(), url) + }) else { + return false; + }; + + let mut task = OpenFileRequest::new(file_path, request); + post_task(ThreadId::FILE_USER_BLOCKING, Some(&mut task)); + + true + } +} + +wrap_task! { + struct OpenFileRequest { + file_path: PathBuf, + request: Arc>, + } + + impl Task { + fn execute(&self) { + debug_assert_ne!( + currently_on(ThreadId::FILE_USER_BLOCKING), + 0, + "execute must be called on the file thread" + ); + + let file_path = self.file_path.display().to_string(); + let Some(stream) = + stream_reader_create_for_file(Some(&CefString::from(file_path.as_str()))) + else { + return; + }; + + // Continue loading on the IO thread. + let mut task = ContinueFileRequest::new(stream, self.request.clone()); + post_task(ThreadId::IO, Some(&mut task)); + } + } +} + +wrap_task! { + struct ContinueFileRequest { + stream: StreamReader, + request: Arc>, + } + + impl Task { + fn execute(&self) { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "execute must be called on the IO thread" + ); + + let Ok(mut request) = self.request.lock() else { + return; + }; + + let handler = StreamResourceHandler::new_with_stream( + (request.mime_type_resolver())(request.url()), + self.stream.clone(), + ); + request.continue_request(Some(handler)); + } + } +} + +struct ArchiveProviderState { + weak_self: Weak>, + url_path: String, + archive_path: PathBuf, + password: String, + load_started: bool, + load_ended: bool, + archive: Option, + pending_requests: Vec>>, +} + +impl ArchiveProviderState { + fn new(url_path: &str, archive_path: PathBuf, password: &str) -> Arc> { + Arc::new_cyclic(|weak_self| { + Mutex::new(Self { + weak_self: weak_self.clone(), + url_path: normalize_url_path(url_path), + archive_path, + password: password.to_string(), + load_started: false, + load_ended: false, + archive: None, + pending_requests: Default::default(), + }) + }) + } + + fn continue_request(&mut self, request: Arc>) -> bool { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "continue_request must be called on the IO thread" + ); + + let Ok(mut request) = request.lock() else { + return false; + }; + + let url = request.url(); + let (Some(archive), Some(relative_path)) = ( + self.archive.as_ref(), + get_file_relative_path(Default::default(), self.url_path.as_str(), url), + ) else { + return false; + }; + + let relative_path = relative_path.display().to_string(); + let Some(file) = archive.file(&relative_path) else { + return false; + }; + let Ok(file) = file.lock() else { + return false; + }; + let Some(stream) = file.stream_reader() else { + return false; + }; + + let handler = + StreamResourceHandler::new_with_stream((request.mime_type_resolver())(url), stream); + request.continue_request(Some(handler)); + true + } +} + +/// Provider of contents loaded from an archive file. +struct ArchiveProvider { + state: Arc>, +} + +impl ArchiveProvider { + fn new_resource_manager_provider( + url_path: &str, + archive_path: PathBuf, + password: &str, + ) -> Box { + Box::new(Self { + state: ArchiveProviderState::new(url_path, archive_path, password), + }) + } +} + +impl ResourceManagerProvider for ArchiveProvider { + fn on_request(&self, request: Arc>) -> bool { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "on_request must be called on the IO thread" + ); + + let (Ok(mut state), Ok(request_lock)) = (self.state.lock(), request.lock()) else { + return false; + }; + let url = request_lock.url(); + if !url.starts_with(state.url_path.as_str()) { + // Not handled by this provider. + return false; + } + + if !state.load_started { + // Initiate archive loading and queue the pending request. + state.load_started = true; + state.pending_requests.push(request.clone()); + + // Load the archive file on the FILE thread. + let mut task = OpenZipRequest::new(state.weak_self.clone()); + post_task(ThreadId::FILE_USER_BLOCKING, Some(&mut task)); + return true; + } + + if state.load_started && !state.load_ended { + // The archive load has already started. Queue the pending request. + state.pending_requests.push(request.clone()); + return true; + } + + // Archive loading is done. + state.continue_request(request.clone()) + } +} + +wrap_task! { + struct OpenZipRequest { + state: Weak>, + } + + impl Task { + fn execute(&self) { + debug_assert_ne!( + currently_on(ThreadId::FILE_USER_BLOCKING), + 0, + "execute must be called on the file thread" + ); + + let Some(state) = self.state.upgrade() else { + return; + }; + let Ok(mut state) = state.lock() else { + return; + }; + + let file_path = state.archive_path.display().to_string(); + let Some(mut stream) = + stream_reader_create_for_file(Some(&CefString::from(file_path.as_str()))) + else { + return; + }; + + let archive = ZipArchive::default(); + if archive.load(&mut stream, state.password.as_str(), true) == 0 { + return; + } + state.archive = Some(archive); + + // Continue loading on the IO thread. + let mut task = ContinueZipRequest::new(state.weak_self.clone()); + post_task(ThreadId::IO, Some(&mut task)); + } + } +} + +wrap_task! { + struct ContinueZipRequest { + state: Weak>, + } + + impl Task { + fn execute(&self) { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "execute must be called on the IO thread" + ); + + let Some(state) = self.state.upgrade() else { + return; + }; + let Ok(mut state) = state.lock() else { + return; + }; + + state.load_ended = true; + + for request in std::mem::take(&mut state.pending_requests) { + state.continue_request(request); + } + } + } +} + +wrap_task! { + struct AddProvider { + manager: Weak>, + provider: Arc>>>, + order: i32, + identifier: String, + } + + impl Task { + fn execute(&self) { + let Some(manager) = self.manager.upgrade() else { + return; + }; + let Ok(mut manager) = manager.lock() else { + return; + }; + let Ok(mut provider) = self.provider.lock() else { + return; + }; + let Some(provider) = provider.take() else { + return; + }; + + manager.add_provider(provider, self.order, &self.identifier); + } + } +} + +wrap_task! { + struct RemoveProviders { + manager: Weak>, + identifier: String, + } + + impl Task { + fn execute(&self) { + let Some(manager) = self.manager.upgrade() else { + return; + }; + let Ok(mut manager) = manager.lock() else { + return; + }; + + manager.remove_providers(&self.identifier); + } + } +} + +wrap_task! { + struct RemoveAllProviders { + manager: Weak>, + } + + impl Task { + fn execute(&self) { + let Some(manager) = self.manager.upgrade() else { + return; + }; + let Ok(mut manager) = manager.lock() else { + return; + }; + + manager.remove_all_providers(); + } + } +} + +wrap_task! { + struct SetUrlFilter { + manager: Weak>, + filter: Arc>>>, + } + + impl Task { + fn execute(&self) { + let Some(manager) = self.manager.upgrade() else { + return; + }; + let Ok(mut manager) = manager.lock() else { + return; + }; + let Ok(mut filter) = self.filter.lock() else { + return; + }; + let Some(filter) = filter.take() else { + return; + }; + + manager.set_url_filter(filter); + } + } +} + +wrap_task! { + struct SetMimeTypeResolver { + manager: Weak>, + resolver: Arc>>>, + } + + impl Task { + fn execute(&self) { + let Some(manager) = self.manager.upgrade() else { + return; + }; + let Ok(mut manager) = manager.lock() else { + return; + }; + let Ok(mut resolver) = self.resolver.lock() else { + return; + }; + let Some(resolver) = resolver.take() else { + return; + }; + + manager.set_mime_type_resolver(resolver); + } + } +} + +/// Type for managing multiple resource providers. For each resource request, providers will be +/// called in order and have the option to: +/// - (a) handle the request by returning a [ResourceHandler], +/// - (b) pass the request to the next provider in order, or +/// - (c) stop handling the request. +/// +/// See comments on the [ResourceManagerRequest] object for additional usage information. The +/// methods of this class may be called on any browser process thread unless otherwise indicated. +pub struct ResourceManager { + providers: VecDeque, + pending_handlers: BTreeMap, + mime_type_resolver: Arc, + url_filter: Arc, + weak_self: Weak>, +} + +impl ResourceManager { + pub fn new() -> Arc> { + Arc::new_cyclic(|weak_self| { + Mutex::new(Self { + providers: Default::default(), + pending_handlers: Default::default(), + mime_type_resolver: Arc::new(Box::new(get_mime_type)), + url_filter: Arc::new(Box::new(get_filtered_url)), + weak_self: weak_self.clone(), + }) + }) + } + + /// Add a provider that maps requests for `url` to `content`. `url` should be fully qualified + /// but not include a query or fragment component. If `mime_type` is empty the [MimeTypeResolver] + /// will be used. See comments on [ResourceManager::add_provider] for usage of the `order` and + /// `identifier` parameters. + pub fn add_content_provider( + &mut self, + url: &str, + content: &str, + mime_type: &str, + order: i32, + identifier: &str, + ) { + self.add_provider( + ContentProvider::new_resource_manager_provider(url, content, mime_type), + order, + identifier, + ) + } + + /// Add a provider that maps requests that start with `url_path` to files under `archive_path`. + /// `url_path` should include an origin and optional path component only. Files will be loaded + /// when a matching URL is requested. See comments on [ResourceManager::add_provider] for usage + /// of the `order` and `identifier` parameters. + pub fn add_directory_provider( + &mut self, + url_path: &str, + directory_path: &str, + order: i32, + identifier: &str, + ) { + self.add_provider( + DirectoryProvider::new_resource_manager_provider(url_path, directory_path.into()), + order, + identifier, + ) + } + + /// Add a provider that maps requests that start with `url_path` to files stored in the archive + /// file at `archive_path`. `url_path` should include an origin and optional path component + /// only. The archive file will be loaded when a matching URL is requested for the first time. + /// See comments on [ResourceManager::add_provider] for usage of the `order` and `identifier` + /// parameters. + pub fn add_archive_provider( + &mut self, + url_path: &str, + archive_path: &str, + password: &str, + order: i32, + identifier: &str, + ) { + self.add_provider( + ArchiveProvider::new_resource_manager_provider(url_path, archive_path.into(), password), + order, + identifier, + ) + } + + /// Add a provider. This object takes ownership of `provider`. Providers will be called in + /// ascending order based on the `order` value. Multiple providers sharing the same `order` + /// value will be called in the order that they were added. The `identifier` value, which does + /// not need to be unique, can be used to remove the provider at a later time. + pub fn add_provider( + &mut self, + provider: Box, + order: i32, + identifier: &str, + ) { + let io_thread_id = ThreadId::IO; + if currently_on(io_thread_id) == 0 { + let mut task = AddProvider::new( + self.weak_self.clone(), + Arc::new(Mutex::new(Some(provider))), + order, + identifier.to_string(), + ); + post_task(io_thread_id, Some(&mut task)); + return; + } + + let provider_entry = ProviderEntry { + provider: Some(provider), + order, + identifier: identifier.to_string(), + ..Default::default() + }; + + if self.providers.is_empty() { + self.providers.push_back(provider_entry); + return; + } + + // Insert before the first entry with a higher `order` value. + let index = self.providers.partition_point(|entry| entry.order < order); + self.providers.insert(index, provider_entry); + } + + /// Remove all providers with the specified `identifier` value. If any removed providers have + /// pending requests the [RequestManagerProvider::on_request_cancel] method will be called. The + /// removed providers may be deleted immediately or at a later time. + pub fn remove_providers(&mut self, identifier: &str) { + let io_thread_id = ThreadId::IO; + if currently_on(io_thread_id) == 0 { + let mut task = RemoveProviders::new(self.weak_self.clone(), identifier.to_string()); + post_task(io_thread_id, Some(&mut task)); + return; + } + + if self.providers.is_empty() { + return; + } + + let mut index = 0; + while index < self.providers.len() { + index = if self.providers[index].identifier == identifier { + self.delete_provider(index, false) + } else { + index + 1 + }; + } + } + + /// Remove all providers. If any removed providers have pending requests the [RequestManagerProvider::on_request_cancel] + /// method will be called. The removed providers may be deleted immediately or at a later time. + pub fn remove_all_providers(&mut self) { + let io_thread_id = ThreadId::IO; + if currently_on(io_thread_id) == 0 { + let mut task = RemoveAllProviders::new(self.weak_self.clone()); + post_task(io_thread_id, Some(&mut task)); + return; + } + + if self.providers.is_empty() { + return; + } + + let mut index = 0; + while index < self.providers.len() { + index = self.delete_provider(index, true); + } + } + + /// Set the url filter. If not set the default no-op filter will be used. Changes to this value + /// will not affect currently pending requests. + pub fn set_url_filter(&mut self, filter: Option) { + let io_thread_id = ThreadId::IO; + if currently_on(io_thread_id) == 0 { + let mut task = + SetUrlFilter::new(self.weak_self.clone(), Arc::new(Mutex::new(Some(filter)))); + post_task(io_thread_id, Some(&mut task)); + return; + } + + self.url_filter = Arc::new(filter.unwrap_or(Box::new(get_filtered_url))); + } + + /// Set the mime type resolver. If not set the default resolver will be used. Changes to this + /// value will not affect currently pending requests. + pub fn set_mime_type_resolver(&mut self, resolver: Option) { + let io_thread_id = ThreadId::IO; + if currently_on(io_thread_id) == 0 { + let mut task = SetMimeTypeResolver::new( + self.weak_self.clone(), + Arc::new(Mutex::new(Some(resolver))), + ); + post_task(io_thread_id, Some(&mut task)); + return; + } + + self.mime_type_resolver = Arc::new(resolver.unwrap_or(Box::new(get_mime_type))); + } + + /// Called from [RequestHandler::on_before_resource_load] on the browser process IO thread. + pub fn on_before_resource_load( + &mut self, + browser: Browser, + frame: Frame, + request: Request, + callback: Callback, + ) -> ReturnValue { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "on_before_resource_load must be called on the IO thread" + ); + + let first_entry = self.get_next_valid_provider(0); + debug_assert!(first_entry <= self.providers.len()); + if first_entry == self.providers.len() { + // No providers so continue the request immediately. + return ReturnValue::CONTINUE; + } + + let url = CefString::from(&request.url()).to_string(); + let url = (*self.url_filter)(&url); + let url = get_url_without_query_or_fragment(&url).to_string(); + let state = RequestState { + manager: self.weak_self.clone(), + callback: Some(callback), + current_entry: first_entry, + current_request: 0, + params: Arc::new(RequestParams { + url, + browser, + frame, + request, + url_filter: self.url_filter.clone(), + mime_type_resolver: self.mime_type_resolver.clone(), + }), + }; + + ReturnValue::from(if self.send_request(state) { + // If the request is potentially handled we need to continue asynchronously. + sys::cef_return_value_t::RV_CONTINUE_ASYNC + } else { + sys::cef_return_value_t::RV_CONTINUE + }) + } + + /// Called from [RequestHandler::resource_handler] on the browser process IO thread. + pub fn resource_handler( + &mut self, + _browser: Browser, + _frame: Frame, + request: Request, + ) -> Option { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "resource_handler must be called on the IO thread" + ); + + self.pending_handlers.remove(&request.identifier()) + } + + /// Send the request to providers in order until one potentially handles it or we run out of + /// providers. Returns true if the request is potentially handled. + fn send_request(&mut self, state: RequestState) -> bool { + let mut potentially_handled = false; + + let current_entry = state.current_entry; + let params = state.params.clone(); + let mut state = Some(state); + + while state.is_some() { + debug_assert!( + current_entry < self.providers.len(), + "Should not be on the last provider entry." + ); + let request = ResourceManagerRequest::new(state.take(), params.clone()); + let Ok(mut request) = request.lock() else { + break; + }; + + // Give the provider an opportunity to handle the request. + state = request.send_request(); + + let Some(mut next_state) = state.take() else { + potentially_handled = true; + break; + }; + + // The provider will not handle the request. Move to the next provider if any. + if self.increment_provider(&mut next_state) { + state = Some(next_state); + } else { + self.stop_request(next_state); + } + } + + potentially_handled + } + + fn continue_request(&mut self, mut state: RequestState, handler: Option) { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "continue_request must be called on the IO thread" + ); + + if let Some(handler) = handler { + // The request has been handled. Associate the request ID with the handler. + self.pending_handlers + .insert(state.params.request.identifier(), handler); + self.stop_request(state); + } else { + // Move to the next provider if any. + if self.increment_provider(&mut state) { + self.send_request(state); + } else { + self.stop_request(state); + } + } + } + + fn stop_request(&mut self, mut state: RequestState) { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "stop_request must be called on the IO thread" + ); + + self.detach_request_from_provider(&mut state); + } + + /// Move state to the next provider if any and return true if there are more providers. + fn increment_provider(&mut self, state: &mut RequestState) -> bool { + let next_entry = self.get_next_valid_provider(state.current_entry); + self.detach_request_from_provider(state); + if next_entry < self.providers.len() { + state.current_entry = next_entry; + true + } else { + false + } + } + + /// The new provider, if any, should be determined before calling this method. + fn detach_request_from_provider(&mut self, state: &mut RequestState) { + // Remove the association from the current provider entry. + let Some(current_entry) = self.providers.get_mut(state.current_entry) else { + return; + }; + current_entry.pending_requests.remove(state.current_request); + if current_entry.deletion_pending && current_entry.pending_requests.is_empty() { + // Delete the current provider entry now. + self.providers.remove(state.current_entry); + } + // Set to the end for error checking purposes. + state.current_entry = self.providers.len(); + } + + /// Move to the next provider that is not pending deletion. + fn get_next_valid_provider(&self, current_provider: usize) -> usize { + self.providers + .iter() + .enumerate() + .skip(current_provider) + .find(|&(_, entry)| !entry.deletion_pending) + .map(|(index, _)| index) + .unwrap_or(self.providers.len()) + } + + fn delete_provider(&mut self, deleted_provider: usize, stop: bool) -> usize { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "delete_provider must be called on the IO thread" + ); + let Some(current_entry) = self.providers.get_mut(deleted_provider) else { + return self.providers.len(); + }; + if current_entry.deletion_pending { + return deleted_provider; + } + if !current_entry.pending_requests.is_empty() { + // Don't delete the provider entry until all pending requests have cleared. + current_entry.deletion_pending = true; + + // Continue pending requests immediately. + for request in current_entry.pending_requests.iter() { + let Ok(mut request_lock) = request.lock() else { + continue; + }; + if request_lock.state.is_some() { + if stop { + request_lock.stop_request(); + } else { + request_lock.continue_request(None); + } + if let Some(provider) = ¤t_entry.provider { + provider.on_request_canceled(request.clone()); + } + } + } + + deleted_provider + 1 + } else { + // Delete the provider entry now. + self.providers.remove(deleted_provider); + deleted_provider + } + } +} + +/// Returns `url` without the query or fragment components, if any. +pub fn get_url_without_query_or_fragment(url: &str) -> &str { + url.split(['?', '#']).next().unwrap_or(url) +} + +/// Determine the mime type based on the `url` file extension. +pub fn get_mime_type(url: &str) -> String { + let url = get_url_without_query_or_fragment(url); + url.rsplit('.') + .next() + .map(|extension| { + let extension = CefString::from(extension); + CefString::from(&crate::get_mime_type(Some(&extension))).to_string() + }) + .unwrap_or_else(|| String::from("text/html")) +} + +/// Default no-op filter. +pub fn get_filtered_url(url: &str) -> String { + url.to_string() +} + +/// Normalize the URL path by adding a trailing slash if it's missing. +pub fn normalize_url_path(url_path: &str) -> String { + format!("{}/", url_path.trim_end_matches('/')) +} + +pub fn get_file_relative_path( + mut base_path: PathBuf, + url_path: &str, + url: &str, +) -> Option { + let segments = url.strip_prefix(url_path)?.split('/'); + for segment in segments { + base_path.push(segment) + } + Some(base_path) +} diff --git a/cef/src/wrapper/stream_resource_handler.rs b/cef/src/wrapper/stream_resource_handler.rs new file mode 100644 index 0000000..f7c30d8 --- /dev/null +++ b/cef/src/wrapper/stream_resource_handler.rs @@ -0,0 +1,121 @@ +//! Implementation of the ResourceHandler class for reading from a Stream. +use crate::*; + +wrap_resource_handler! { + pub struct StreamResourceHandler { + status_code: i32, + status_text: String, + mime_type: String, + header_map: Option, + stream: Option, + } + + impl ResourceHandler { + fn open( + &self, + _request: Option<&mut Request>, + handle_request: Option<&mut i32>, + _callback: Option<&mut Callback>, + ) -> i32 { + debug_assert_eq!( + currently_on(ThreadId::UI), + 0, + "open must not be called on the UI thread" + ); + debug_assert_eq!( + currently_on(ThreadId::IO), + 0, + "open must not be called on the IO thread" + ); + + // Continue the request immediately. + if let Some(handle_request) = handle_request { + *handle_request = 1; + } + + 1 + } + + fn response_headers( + &self, + response: Option<&mut Response>, + response_length: Option<&mut i64>, + _redirect_url: Option<&mut CefString>, + ) { + debug_assert_ne!( + currently_on(ThreadId::IO), + 0, + "response_headers must be called on the IO thread" + ); + + let Some(response) = response else { + return; + }; + + response.set_status(self.status_code); + response.set_status_text(Some(&CefString::from(self.status_text.as_str()))); + response.set_mime_type(Some(&CefString::from(self.mime_type.as_str()))); + + if let Some(mut header_map) = self.header_map.clone() { + response.set_header_map(Some(&mut header_map)); + } + + if let Some(response_length) = response_length { + *response_length = if self.stream.is_some() { -1 } else { 0 }; + } + } + + #[allow(clippy::not_unsafe_ptr_arg_deref)] + fn read( + &self, + data_out: *mut u8, + bytes_to_read: i32, + bytes_read: Option<&mut i32>, + _callback: Option<&mut ResourceReadCallback>, + ) -> i32 { + debug_assert_eq!( + currently_on(ThreadId::UI), + 0, + "read must not be called on the UI thread" + ); + debug_assert_eq!( + currently_on(ThreadId::IO), + 0, + "read must not be called on the IO thread" + ); + if bytes_to_read < 1 { + return 0; + } + let Some(bytes_read) = bytes_read else { + return 0; + }; + let Some(stream) = &self.stream else { + *bytes_read = 0; + return 1; + }; + + // Read until the buffer is full or until read() returns 0 to indicate no more data. + *bytes_read = 0; + loop { + let data_out = unsafe { data_out.add(*bytes_read as usize) }; + let read = stream.read(data_out, 1, (bytes_to_read - *bytes_read) as usize); + *bytes_read += read as i32; + if read == 0 || *bytes_read >= bytes_to_read { + break; + } + } + + if *bytes_read > 0 { + 1 + } else { + 0 + } + } + } +} + +impl StreamResourceHandler { + pub fn new_with_stream(mime_type: String, stream: StreamReader) -> ResourceHandler { + Self::new(200, "OK".to_string(), mime_type, None, Some(stream)) + } +} diff --git a/cef/src/wrapper/zip_archive.rs b/cef/src/wrapper/zip_archive.rs new file mode 100644 index 0000000..62d4e12 --- /dev/null +++ b/cef/src/wrapper/zip_archive.rs @@ -0,0 +1,131 @@ +use super::byte_read_handler::*; +use crate::*; +use std::{ + collections::BTreeMap, + sync::{Arc, Mutex}, +}; + +pub trait File: Send { + fn stream_reader(&self) -> Option; +} + +pub type FileMap = BTreeMap>>; + +struct ZipFile { + data: Vec, +} + +impl File for ZipFile { + fn stream_reader(&self) -> Option { + let mut handler = + ByteReadHandler::new(Arc::new(Mutex::new(ByteStream::new(self.data.clone())))); + stream_reader_create_for_handler(Some(&mut handler)) + } +} + +#[derive(Default)] +pub struct ZipArchive { + contents: Mutex, +} + +impl ZipArchive { + pub fn load( + &self, + stream: &mut StreamReader, + password: &str, + overwrite_existing: bool, + ) -> usize { + let Ok(mut contents) = self.contents.lock() else { + return 0; + }; + let Some(reader) = zip_reader_create(Some(stream)) else { + return 0; + }; + let password = CefString::from(password); + + let mut count = 0; + loop { + let size = reader.file_size(); + if size <= 0 { + // Skip directories and empty files. + continue; + } + let size = size as usize; + + if reader.open_file(Some(&password)) == 0 { + break; + } + + let name = CefString::from(&reader.file_name()) + .to_string() + .to_lowercase(); + + if contents.contains_key(&name) { + if overwrite_existing { + contents.remove(&name); + } else { + // Skip files that already exist. + continue; + } + } + + let mut data = Vec::with_capacity(size); + while data.len() < size && reader.eof() == 0 { + let mut chunk = vec![0; size - data.len()]; + let read = reader.read_file(Some(&mut chunk)); + if read <= 0 { + break; + } + data.extend_from_slice(&chunk[..read as usize]); + } + + debug_assert_eq!(data.len(), size); + reader.close_file(); + count += 1; + + // Add the file to the map. + contents.insert(name, Arc::new(Mutex::new(ZipFile { data }))); + + if reader.move_to_next_file() == 0 { + break; + } + } + + count + } + + pub fn clear(&self) { + let Ok(mut contents) = self.contents.lock() else { + return; + }; + contents.clear(); + } + + pub fn file_count(&self) -> usize { + let Ok(contents) = self.contents.lock() else { + return 0; + }; + contents.len() + } + + pub fn file(&self, name: &str) -> Option>> { + let Ok(contents) = self.contents.lock() else { + return None; + }; + contents.get(name).cloned() + } + + pub fn remove_file(&self, name: &str) -> bool { + let Ok(mut contents) = self.contents.lock() else { + return false; + }; + contents.remove(name).is_some() + } + + pub fn files(&self) -> FileMap { + let Ok(contents) = self.contents.lock() else { + return Default::default(); + }; + contents.clone() + } +} diff --git a/download-cef/src/lib.rs b/download-cef/src/lib.rs index feab4f6..f9e5ac2 100644 --- a/download-cef/src/lib.rs +++ b/download-cef/src/lib.rs @@ -357,7 +357,7 @@ impl CefVersion { where P: AsRef, { - let mut result = self.download_archive_from(&url, &location, show_progress); + let mut result = self.download_archive_from(url, &location, show_progress); let mut retry = 0; while let Err(Error::Io(_)) = &result { @@ -368,7 +368,7 @@ impl CefVersion { retry += 1; thread::sleep(retry_delay * retry); - result = self.download_archive_from(&url, &location, show_progress); + result = self.download_archive_from(url, &location, show_progress); } result @@ -459,7 +459,7 @@ where println!("Downloading CEF archive for {target}..."); } - let index = CefIndex::download_from(&url)?; + let index = CefIndex::download_from(url)?; let platform = index.platform(target)?; let version = platform.version(cef_version)?; diff --git a/examples/cefsimple/Cargo.toml b/examples/cefsimple/Cargo.toml index e4e5ff1..ab347ae 100644 --- a/examples/cefsimple/Cargo.toml +++ b/examples/cefsimple/Cargo.toml @@ -3,27 +3,55 @@ name = "cefsimple" edition = "2024" publish = false +[lib] +crate-type = ["cdylib"] + [[bin]] name = "cefsimple" [[bin]] name = "cefsimple_helper" -path = "src/mac/helper.rs" - -[[bin]] -name = "bundle_cefsimple" -path = "src/mac/bundle_cefsimple.rs" [features] -default = ["sandbox"] -sandbox = ["cef/sandbox"] +default = [ + "sandbox", +] +sandbox = [ + "cef/sandbox", +] +linux-x11 = [ + "x11-dl", +] + +[package.metadata.cef.bundle] +helper_name = "cefsimple_helper" +resources_path = "resources" [dependencies] -cef.workspace = true -cef-dll-sys.workspace = true +cef = { workspace = true, features = [ + "build-util", +] } [target.'cfg(target_os = "macos")'.dependencies] -plist.workspace = true -serde.workspace = true objc2.workspace = true -objc2-app-kit = { workspace = true, features = ["NSApplication", "NSResponder"] } +objc2-foundation.workspace = true +objc2-app-kit = { workspace = true, features = [ + "NSApplication", + "NSEvent", + "NSResponder", + "NSUserInterfaceValidation", + "NSView", + "NSWindow", +] } + +[target.'cfg(target_os = "windows")'.dependencies] +windows-sys = { workspace = true, features = [ + "Win32_Foundation", + "Win32_UI_WindowsAndMessaging", +] } + +[target.'cfg(target_os = "windows")'.build-dependencies] +winres.workspace = true + +[target.'cfg(target_os = "linux")'.dependencies] +x11-dl = { workspace = true, optional = true } diff --git a/examples/cefsimple/build.rs b/examples/cefsimple/build.rs new file mode 100644 index 0000000..56e86eb --- /dev/null +++ b/examples/cefsimple/build.rs @@ -0,0 +1,14 @@ +#[cfg(target_os = "windows")] +include!("src/shared/resources.rs"); + +#[cfg(target_os = "windows")] +fn main() { + winres::WindowsResource::new() + .set_icon_with_id("resources/win/cefsimple.ico", &IDI_CEFSIMPLE.to_string()) + .set_icon_with_id("resources/win/small.ico", &IDI_SMALL.to_string()) + .compile() + .unwrap(); +} + +#[cfg(not(target_os = "windows"))] +fn main() {} diff --git a/examples/cefsimple/resources/mac/English.lproj/InfoPlist.strings b/examples/cefsimple/resources/mac/English.lproj/InfoPlist.strings new file mode 100644 index 0000000..dc5ebb0 --- /dev/null +++ b/examples/cefsimple/resources/mac/English.lproj/InfoPlist.strings @@ -0,0 +1,3 @@ +/* Localized versions of Info.plist keys */ + +NSHumanReadableCopyright = "© Chromium Embedded Framework Authors, 2013"; diff --git a/examples/cefsimple/resources/mac/English.lproj/MainMenu.xib b/examples/cefsimple/resources/mac/English.lproj/MainMenu.xib new file mode 100644 index 0000000..7eff402 --- /dev/null +++ b/examples/cefsimple/resources/mac/English.lproj/MainMenu.xib @@ -0,0 +1,330 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/cefsimple/resources/mac/cefsimple.icns b/examples/cefsimple/resources/mac/cefsimple.icns new file mode 100644 index 0000000000000000000000000000000000000000..f36742de2572421b1eaa5569f64ab34e450ddb67 GIT binary patch literal 55080 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%0Ac$%Pvh#VA!uK`rp4)7 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 OV{TRe000a^@Bjd~o7PtV literal 0 HcmV?d00001 diff --git a/examples/cefsimple/resources/win/cefsimple.ico b/examples/cefsimple/resources/win/cefsimple.ico new file mode 100644 index 0000000000000000000000000000000000000000..d551aa3aaf80adf9b7760e2eb8de95a5c3e53df6 GIT binary patch literal 23558 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, - #[serde(rename = "LSFileQuarantineEnabled")] - ls_file_quarantine_enabled: bool, - #[serde(rename = "LSMinimumSystemVersion")] - ls_minimum_system_version: String, - #[serde(rename = "LSUIElement")] - ls_ui_element: Option, - #[serde(rename = "NSBluetoothAlwaysUsageDescription")] - ns_bluetooth_always_usage_description: String, - #[serde(rename = "NSSupportsAutomaticGraphicsSwitching")] - ns_supports_automatic_graphics_switching: bool, - #[serde(rename = "NSWebBrowserPublicKeyCredentialUsageDescription")] - ns_web_browser_publickey_credential_usage_description: String, - #[serde(rename = "NSCameraUsageDescription")] - ns_camera_usage_description: String, - #[serde(rename = "NSMicrophoneUsageDescription")] - ns_microphone_usage_description: String, - } - - const EXEC_PATH: &str = "Contents/MacOS"; - const FRAMEWORKS_PATH: &str = "Contents/Frameworks"; - const RESOURCES_PATH: &str = "Contents/Resources"; - const FRAMEWORK: &str = "Chromium Embedded Framework.framework"; - const HELPERS: &[&str] = &[ - "cefsimple Helper (GPU)", - "cefsimple Helper (Renderer)", - "cefsimple Helper (Plugin)", - "cefsimple Helper (Alerts)", - "cefsimple Helper", - ]; - - fn create_app_layout(app_path: &Path) -> PathBuf { - [EXEC_PATH, RESOURCES_PATH, FRAMEWORKS_PATH] - .iter() - .for_each(|p| fs::create_dir_all(app_path.join(p)).unwrap()); - app_path.join("Contents") - } - - fn create_app(app_path: &Path, exec_name: &str, bin: &Path, is_helper: bool) -> PathBuf { - let app_path = app_path.join(exec_name).with_extension("app"); - let contents_path = create_app_layout(&app_path); - create_info_plist(&contents_path, exec_name, is_helper).unwrap(); - fs::copy(bin, app_path.join(EXEC_PATH).join(exec_name)).unwrap(); - app_path - } - - // See https://bitbucket.org/chromiumembedded/cef/wiki/GeneralUsage.md#markdown-header-macos - fn bundle(app_path: &Path) { - let example_path = PathBuf::from(app_path); - let main_app_path = create_app( - app_path, - "cefsimple", - &example_path.join("cefsimple"), - false, - ); - let cef_path = cef_dll_sys::get_cef_dir().unwrap(); - let to = main_app_path.join(FRAMEWORKS_PATH).join(FRAMEWORK); - if to.exists() { - fs::remove_dir_all(&to).unwrap(); - } - copy_directory(&cef_path.join(FRAMEWORK), &to); - HELPERS.iter().for_each(|helper| { - create_app( - &main_app_path.join(FRAMEWORKS_PATH), - helper, - &example_path.join("cefsimple_helper"), - true, - ); - }); - } - - fn create_info_plist( - contents_path: &Path, - exec_name: &str, - is_helper: bool, - ) -> Result<(), Box> { - let info_plist = InfoPlist { - cf_bundle_development_region: "en".to_string(), - cf_bundle_display_name: exec_name.to_string(), - cf_bundle_executable: exec_name.to_string(), - cf_bundle_identifier: "org.cef-rs.cefsimple.helper".to_string(), - cf_bundle_info_dictionary_version: "6.0".to_string(), - cf_bundle_name: "cef-rs".to_string(), - cf_bundle_package_type: "APPL".to_string(), - cf_bundle_signature: "????".to_string(), - cf_bundle_version: "1.0.0".to_string(), - cf_bundle_short_version_string: "1.0".to_string(), - ls_environment: [("MallocNanoZone".to_string(), "0".to_string())] - .iter() - .cloned() - .collect(), - ls_file_quarantine_enabled: true, - ls_minimum_system_version: "11.0".to_string(), - ls_ui_element: if is_helper { - Some("1".to_string()) - } else { - None - }, - ns_bluetooth_always_usage_description: exec_name.to_string(), - ns_supports_automatic_graphics_switching: true, - ns_web_browser_publickey_credential_usage_description: exec_name.to_string(), - ns_camera_usage_description: exec_name.to_string(), - ns_microphone_usage_description: exec_name.to_string(), - }; - - plist::to_file_xml(contents_path.join("Info.plist"), &info_plist)?; - Ok(()) - } - - fn copy_directory(src: &Path, dst: &Path) { - fs::create_dir_all(dst).unwrap(); - for entry in fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let dst_path = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_directory(&entry.path(), &dst_path); - } else { - fs::copy(entry.path(), &dst_path).unwrap(); - } - } - } - - fn run_command(args: &[&str]) -> Result<(), Box> { - let status = Command::new("cargo") - .args(args) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .status()?; - - if !status.success() { - std::process::exit(1); - } - Ok(()) - } - - pub fn main() -> Result<(), Box> { - let app_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/debug"); - run_command(&["build", "--bin", "cefsimple"])?; - run_command(&["build", "--bin", "cefsimple_helper"])?; - bundle(&app_path); - Ok(()) - } -} - -#[cfg(target_os = "macos")] -fn main() -> Result<(), Box> { - mac::main() -} - -#[cfg(not(target_os = "macos"))] -fn main() {} diff --git a/examples/cefsimple/src/mac/mod.rs b/examples/cefsimple/src/mac/mod.rs new file mode 100644 index 0000000..865d66c --- /dev/null +++ b/examples/cefsimple/src/mac/mod.rs @@ -0,0 +1,243 @@ +use crate::shared::simple_handler::*; +use cef::application_mac::{CefAppProtocol, CrAppControlProtocol, CrAppProtocol}; +use objc2::{ + ClassType, DefinedClass, MainThreadMarker, MainThreadOnly, define_class, extern_methods, + msg_send, + rc::Retained, + runtime::{AnyObject, Bool, NSObject, NSObjectProtocol, ProtocolObject}, + sel, +}; +use objc2_app_kit::{ + NSApp, NSApplication, NSApplicationDelegate, NSApplicationTerminateReply, NSEvent, + NSUserInterfaceValidations, NSValidatedUserInterfaceItem, +}; +use objc2_foundation::{NSBundle, NSObjectNSThreadPerformAdditions, ns_string}; +use std::{cell::Cell, ptr}; + +define_class! { + #[unsafe(super(NSObject))] + #[thread_kind = MainThreadOnly] + pub struct SimpleAppDelegate; + + impl SimpleAppDelegate { + /// Create the application on the UI thread. + #[unsafe(method(createApplication:))] + unsafe fn create_application(&self, _object: Option<&AnyObject>) { + let app = NSApp(MainThreadMarker::new().expect("Not running on the main thread")); + assert!(app.isKindOfClass(SimpleApplication::class())); + assert!( + app.delegate() + .unwrap() + .isKindOfClass(SimpleAppDelegate::class()) + ); + + let main_bundle = NSBundle::mainBundle(); + let _: Bool = msg_send![&main_bundle, + loadNibNamed: ns_string!("MainMenu"), + owner: &*app, + topLevelObjects: ptr::null_mut::<*const AnyObject>() + ]; + } + } + + unsafe impl NSObjectProtocol for SimpleAppDelegate {} + + unsafe impl NSApplicationDelegate for SimpleAppDelegate { + #[unsafe(method(applicationShouldTerminate:))] + unsafe fn application_should_terminate(&self, _sender: &NSApplication) -> NSApplicationTerminateReply { + NSApplicationTerminateReply::TerminateNow + } + + /// Called when the user clicks the app dock icon while the application is + /// already running. + #[unsafe(method(applicationShouldHandleReopen:hasVisibleWindows:))] + unsafe fn application_should_handle_reopen(&self, _sender: &NSApplication, _has_visible_windows: Bool) -> Bool { + if let Some(handler) = SimpleHandler::instance() { + let mut handler = handler.lock().expect("Failed to lock SimpleHandler"); + if !handler.is_closing() { + handler.show_main_window(); + } + } + Bool::NO + } + + /// Requests that any state restoration archive be created with secure encoding + /// (macOS 12+ only). See https://crrev.com/c737387656 for details. This also + /// fixes an issue with macOS default behavior incorrectly restoring windows + /// after hard reset (holding down the power button). + #[unsafe(method(applicationSupportsSecureRestorableState:))] + unsafe fn application_supports_secure_restorable_state(&self, _sender: &NSApplication) -> Bool { + Bool::YES + } + } + + unsafe impl NSUserInterfaceValidations for SimpleAppDelegate { + #[unsafe(method(validateUserInterfaceItem:))] + unsafe fn validate_user_interface_item(&self, item: &ProtocolObject) -> Bool { + const IDC_FIND: isize = 37000; + + let tag = item.tag(); + if tag == IDC_FIND { + Bool::YES + } else { + Bool::NO + } + } + } +} + +impl SimpleAppDelegate { + fn new(mtm: MainThreadMarker) -> Retained { + let this = SimpleAppDelegate::alloc(mtm).set_ivars(()); + unsafe { msg_send![super(this), init] } + } +} + +/// Instance variables of `SimpleApplication`. +#[derive(Default)] +pub struct SimpleApplicationIvars { + handling_send_event: Cell, +} + +define_class!( + /// A `NSApplication` subclass that implements the required CEF protocols. + /// + /// This class provides the necessary `CefAppProtocol` conformance to + /// ensure that events are handled correctly by the Chromium framework on macOS. + #[unsafe(super(NSApplication))] + #[ivars = SimpleApplicationIvars] + pub struct SimpleApplication; + + impl SimpleApplication { + #[unsafe(method(sendEvent:))] + unsafe fn send_event(&self, event: &NSEvent) { + let was_sending_event = self.is_handling_send_event(); + if !was_sending_event { + self.set_handling_send_event(true); + } + + let _: () = msg_send![super(self), sendEvent:event]; + + if !was_sending_event { + self.set_handling_send_event(false); + } + } + + /// |-terminate:| is the entry point for orderly "quit" operations in Cocoa. This + /// includes the application menu's quit menu item and keyboard equivalent, the + /// application's dock icon menu's quit menu item, "quit" (not "force quit") in + /// the Activity Monitor, and quits triggered by user logout and system restart + /// and shutdown. + /// + /// The default |-terminate:| implementation ends the process by calling exit(), + /// and thus never leaves the main run loop. This is unsuitable for Chromium + /// since Chromium depends on leaving the main run loop to perform an orderly + /// shutdown. We support the normal |-terminate:| interface by overriding the + /// default implementation. Our implementation, which is very specific to the + /// needs of Chromium, works by asking the application delegate to terminate + /// using its |-tryToTerminateApplication:| method. + /// + /// |-tryToTerminateApplication:| differs from the standard + /// |-applicationShouldTerminate:| in that no special event loop is run in the + /// case that immediate termination is not possible (e.g., if dialog boxes + /// allowing the user to cancel have to be shown). Instead, this method tries to + /// close all browsers by calling CloseBrowser(false) via + /// ClientHandler::CloseAllBrowsers. Calling CloseBrowser will result in a call + /// to ClientHandler::DoClose and execution of |-performClose:| on the NSWindow. + /// DoClose sets a flag that is used to differentiate between new close events + /// (e.g., user clicked the window close button) and in-progress close events + /// (e.g., user approved the close window dialog). The NSWindowDelegate + /// |-windowShouldClose:| method checks this flag and either calls + /// CloseBrowser(false) in the case of a new close event or destructs the + /// NSWindow in the case of an in-progress close event. + /// ClientHandler::OnBeforeClose will be called after the CEF NSView hosted in + /// the NSWindow is dealloc'ed. + /// + /// After the final browser window has closed ClientHandler::OnBeforeClose will + /// begin actual tear-down of the application by calling CefQuitMessageLoop. + /// This ends the NSApplication event loop and execution then returns to the + /// main() function for cleanup before application termination. + /// + /// The standard |-applicationShouldTerminate:| is not supported, and code paths + /// leading to it must be redirected. + #[unsafe(method(terminate:))] + unsafe fn terminate(&self, _sender: &AnyObject) { + if let Some(handler) = SimpleHandler::instance() { + let mut handler = handler.lock().expect("Failed to lock SimpleHandler"); + if !handler.is_closing() { + handler.close_all_browsers(false); + } + } + } + } + + unsafe impl CrAppControlProtocol for SimpleApplication { + #[unsafe(method(setHandlingSendEvent:))] + unsafe fn _set_handling_send_event(&self, handling_send_event: Bool) { + self.ivars().handling_send_event.set(handling_send_event); + } + } + + unsafe impl CrAppProtocol for SimpleApplication { + #[unsafe(method(isHandlingSendEvent))] + unsafe fn _is_handling_send_event(&self) -> Bool { + self.ivars().handling_send_event.get() + } + } + + unsafe impl CefAppProtocol for SimpleApplication {} +); + +impl SimpleApplication { + extern_methods! { + #[unsafe(method(sharedApplication))] + fn shared_application() -> Retained; + + #[unsafe(method(setHandlingSendEvent:))] + fn set_handling_send_event(&self, handling_send_event: bool); + + #[unsafe(method(isHandlingSendEvent))] + fn is_handling_send_event(&self) -> bool; + } +} + +pub fn setup_simple_application() { + // Initialize the SimpleApplication instance. + // SAFETY: mtm ensures that here is the main thread. + let _ = SimpleApplication::shared_application(); + + // If there was an invocation to NSApp prior to here, + // then the NSApp will not be a SimpleApplication. + // The following assertion ensures that this doesn't happen. + assert!( + NSApp(MainThreadMarker::new().expect("Not running on the main thread")) + .isKindOfClass(SimpleApplication::class()) + ); +} + +pub fn setup_simple_app_delegate() -> Retained { + let mtm = MainThreadMarker::new().expect("Not running on the main thread"); + + // Create the application delegate. + let simple_delegate = SimpleAppDelegate::new(mtm); + let delegate_proto = + ProtocolObject::::from_retained(simple_delegate.clone()); + let app = NSApp(MainThreadMarker::new().expect("Not running on the main thread")); + assert!(app.isKindOfClass(SimpleApplication::class())); + app.setDelegate(Some(&delegate_proto)); + assert!( + app.delegate() + .unwrap() + .isKindOfClass(SimpleAppDelegate::class()) + ); + + unsafe { + simple_delegate.performSelectorOnMainThread_withObject_waitUntilDone( + sel!(createApplication:), + None, + false, + ); + } + + simple_delegate +} diff --git a/examples/cefsimple/src/main.rs b/examples/cefsimple/src/main.rs index c53c645..fd5ccd8 100644 --- a/examples/cefsimple/src/main.rs +++ b/examples/cefsimple/src/main.rs @@ -1,230 +1,27 @@ -use cef::{args::Args, rc::*, *}; -use std::sync::{Arc, Mutex}; +#![cfg_attr( + all(not(debug_assertions), not(feature = "sandbox"), target_os = "windows"), + windows_subsystem = "windows" +)] -wrap_app! { - struct DemoApp { - window: Arc>>, - } - - impl App { - fn browser_process_handler(&self) -> Option { - Some(DemoBrowserProcessHandler::new( - self.window.clone(), - )) - } - } -} - -wrap_browser_process_handler! { - struct DemoBrowserProcessHandler { - window: Arc>>, - } - - impl BrowserProcessHandler { - // The real lifespan of cef starts from `on_context_initialized`, so all the cef objects should be manipulated after that. - fn on_context_initialized(&self) { - println!("cef context intiialized"); - let mut client = DemoClient::new(); - let url = CefString::from("https://www.google.com"); - - let browser_view = browser_view_create( - Some(&mut client), - Some(&url), - Some(&Default::default()), - Option::<&mut DictionaryValue>::None, - Option::<&mut RequestContext>::None, - Option::<&mut BrowserViewDelegate>::None, - ) - .expect("Failed to create browser view"); - - let mut delegate = DemoWindowDelegate::new(browser_view); - if let Ok(mut window) = self.window.lock() { - *window = Some( - window_create_top_level(Some(&mut delegate)).expect("Failed to create window"), - ); - } - } - } -} - -wrap_client! { - struct DemoClient; - impl Client {} -} - -wrap_window_delegate! { - struct DemoWindowDelegate { - browser_view: BrowserView, - } - - impl ViewDelegate { - fn on_child_view_changed( - &self, - _view: Option<&mut View>, - _added: ::std::os::raw::c_int, - _child: Option<&mut View>, - ) { - // view.as_panel().map(|x| x.as_window().map(|w| w.close())); - } - } - - impl PanelDelegate {} - - impl WindowDelegate { - fn on_window_created(&self, window: Option<&mut Window>) { - if let Some(window) = window { - let view = self.browser_view.clone(); - window.add_child_view(Some(&mut (&view).into())); - window.show(); - } - } - - fn on_window_destroyed(&self, _window: Option<&mut Window>) { - quit_message_loop(); - } - - fn with_standard_window_buttons(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - 1 - } - - fn can_resize(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - 1 - } - - fn can_maximize(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - 1 - } - - fn can_minimize(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - 1 - } - - fn can_close(&self, _window: Option<&mut Window>) -> ::std::os::raw::c_int { - 1 - } - } -} - -// FIXME: Rewrite this demo based on cef/tests/cefsimple -fn main() { - #[cfg(target_os = "macos")] - let _loader = { - let loader = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), false); - assert!(loader.load()); - loader - }; - - #[cfg(target_os = "macos")] - { - use objc2::{ - ClassType, MainThreadMarker, msg_send, - rc::Retained, - runtime::{AnyObject, NSObjectProtocol}, - }; - use objc2_app_kit::NSApp; - - use application::SimpleApplication; - - let mtm = MainThreadMarker::new().unwrap(); - - unsafe { - // Initialize the SimpleApplication instance. - // SAFETY: mtm ensures that here is the main thread. - let _: Retained = msg_send![SimpleApplication::class(), sharedApplication]; - } - - // If there was an invocation to NSApp prior to here, - // then the NSApp will not be a SimpleApplication. - // The following assertion ensures that this doesn't happen. - assert!(NSApp(mtm).isKindOfClass(SimpleApplication::class())); - } - - let _ = api_hash(sys::CEF_API_VERSION_LAST, 0); - - let args = Args::new(); - let cmd = args.as_cmd_line().unwrap(); - - let switch = CefString::from("type"); - let is_browser_process = cmd.has_switch(Some(&switch)) != 1; - - let window = Arc::new(Mutex::new(None)); - let mut app = DemoApp::new(window.clone()); - - let ret = execute_process( - Some(args.as_main_args()), - Some(&mut app), - std::ptr::null_mut(), - ); - - if is_browser_process { - println!("launch browser process"); - assert!(ret == -1, "cannot execute browser process"); - } else { - let process_type = CefString::from(&cmd.switch_value(Some(&switch))); - println!("launch process {process_type}"); - assert!(ret >= 0, "cannot execute non-browser process"); - // non-browser process does not initialize cef - return; - } - let settings = Settings { - no_sandbox: !cfg!(feature = "sandbox") as _, - ..Default::default() - }; - assert_eq!( - initialize( - Some(args.as_main_args()), - Some(&settings), - Some(&mut app), - std::ptr::null_mut(), - ), - 1 - ); - - run_message_loop(); - - let window = window.lock().expect("Failed to lock window"); - let window = window.as_ref().expect("Window is None"); - assert!(window.has_one_ref()); - - shutdown(); -} +pub mod shared; #[cfg(target_os = "macos")] -mod application { - use std::cell::Cell; +mod mac; - use cef::application_mac::{CefAppProtocol, CrAppControlProtocol, CrAppProtocol}; - use objc2::{DefinedClass, define_class, runtime::Bool}; - use objc2_app_kit::NSApplication; +#[cfg(not(all(feature = "sandbox", target_os = "windows")))] +fn main() -> Result<(), &'static str> { + let _library = shared::load_cef(); - /// Instance variables of `SimpleApplication`. - pub struct SimpleApplicationIvars { - handling_send_event: Cell, - } + let args = cef::args::Args::new(); + let Some(cmd_line) = args.as_cmd_line() else { + return Err("Failed to parse command line arguments"); + }; - define_class!( - /// A `NSApplication` subclass that implements the required CEF protocols. - /// - /// This class provides the necessary `CefAppProtocol` conformance to - /// ensure that events are handled correctly by the Chromium framework on macOS. - #[unsafe(super(NSApplication))] - #[ivars = SimpleApplicationIvars] - pub struct SimpleApplication; - - unsafe impl CrAppControlProtocol for SimpleApplication { - #[unsafe(method(setHandlingSendEvent:))] - unsafe fn set_handling_send_event(&self, handling_send_event: Bool) { - self.ivars().handling_send_event.set(handling_send_event); - } - } - - unsafe impl CrAppProtocol for SimpleApplication { - #[unsafe(method(isHandlingSendEvent))] - unsafe fn is_handling_send_event(&self) -> Bool { - self.ivars().handling_send_event.get() - } - } - - unsafe impl CefAppProtocol for SimpleApplication {} - ); + shared::run_main(args.as_main_args(), &cmd_line, std::ptr::null_mut()); + Ok(()) +} + +#[cfg(all(feature = "sandbox", target_os = "windows"))] +fn main() -> Result<(), &'static str> { + Err("Running in sandbox mode on Windows requires bootstrap.exe or bootstrapc.exe.") } diff --git a/examples/cefsimple/src/shared/mod.rs b/examples/cefsimple/src/shared/mod.rs new file mode 100644 index 0000000..0bfe251 --- /dev/null +++ b/examples/cefsimple/src/shared/mod.rs @@ -0,0 +1,75 @@ +//! Rust port of the [`cefsimple`](https://github.com/chromiumembedded/cef/tree/master/tests/cefsimple) example. + +use cef::*; + +pub mod resources; +pub mod simple_app; +pub mod simple_handler; + +#[cfg(target_os = "macos")] +pub type Library = library_loader::LibraryLoader; + +#[cfg(not(target_os = "macos"))] +pub type Library = (); + +#[allow(dead_code)] +pub fn load_cef() -> Library { + #[cfg(target_os = "macos")] + let library = { + let loader = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), false); + assert!(loader.load()); + loader + }; + #[cfg(not(target_os = "macos"))] + let library = (); + + // Initialize the CEF API version. + let _ = api_hash(sys::CEF_API_VERSION_LAST, 0); + + #[cfg(target_os = "macos")] + crate::mac::setup_simple_application(); + + library +} + +#[allow(dead_code)] +pub fn run_main(main_args: &MainArgs, cmd_line: &CommandLine, sandbox_info: *mut u8) { + let switch = CefString::from("type"); + let is_browser_process = cmd_line.has_switch(Some(&switch)) != 1; + + let ret = execute_process(Some(main_args), None, sandbox_info); + + if is_browser_process { + println!("launch browser process"); + assert_eq!(ret, -1, "cannot execute browser process"); + } else { + let process_type = CefString::from(&cmd_line.switch_value(Some(&switch))); + println!("launch process {process_type}"); + assert!(ret >= 0, "cannot execute non-browser process"); + // non-browser process does not initialize cef + return; + } + + let mut app = simple_app::SimpleApp::new(); + + let settings = Settings { + no_sandbox: !cfg!(feature = "sandbox") as _, + ..Default::default() + }; + assert_eq!( + initialize( + Some(main_args), + Some(&settings), + Some(&mut app), + sandbox_info, + ), + 1 + ); + + #[cfg(target_os = "macos")] + let _delegate = crate::mac::setup_simple_app_delegate(); + + run_message_loop(); + + shutdown(); +} diff --git a/examples/cefsimple/src/shared/resources.rs b/examples/cefsimple/src/shared/resources.rs new file mode 100644 index 0000000..4f96666 --- /dev/null +++ b/examples/cefsimple/src/shared/resources.rs @@ -0,0 +1,2 @@ +pub const IDI_CEFSIMPLE: u16 = 120; +pub const IDI_SMALL: u16 = 121; diff --git a/examples/cefsimple/src/shared/simple_app.rs b/examples/cefsimple/src/shared/simple_app.rs new file mode 100644 index 0000000..b27da29 --- /dev/null +++ b/examples/cefsimple/src/shared/simple_app.rs @@ -0,0 +1,215 @@ +use cef::*; +use std::cell::RefCell; + +use super::simple_handler::*; + +wrap_window_delegate! { + struct SimpleWindowDelegate { + browser_view: RefCell>, + runtime_style: RuntimeStyle, + initial_show_state: ShowState, + } + + impl ViewDelegate { + fn preferred_size(&self, _view: Option<&mut View>) -> Size { + Size { + width: 800, + height: 600, + } + } + } + + impl PanelDelegate {} + + impl WindowDelegate { + fn on_window_created(&self, window: Option<&mut Window>) { + // Add the browser view and show the window. + let browser_view = self.browser_view.borrow(); + let (Some(window), Some(browser_view)) = (window, browser_view.as_ref()) else { + return; + }; + let mut view = View::from(browser_view); + window.add_child_view(Some(&mut view)); + + if self.initial_show_state != ShowState::HIDDEN { + window.show(); + } + } + + fn on_window_destroyed(&self, _window: Option<&mut Window>) { + let mut browser_view = self.browser_view.borrow_mut(); + *browser_view = None; + } + + fn can_close(&self, _window: Option<&mut Window>) -> i32 { + // Allow the window to close if the browser says it's OK. + let browser_view = self.browser_view.borrow(); + let browser_view = browser_view.as_ref().expect("BrowserView is None"); + if let Some(browser) = browser_view.browser() { + let browser_host = browser.host().expect("BrowserHost is None"); + browser_host.try_close_browser() + } else { + 1 + } + } + + fn initial_show_state(&self, _window: Option<&mut Window>) -> ShowState { + self.initial_show_state + } + + fn window_runtime_style(&self) -> RuntimeStyle { + self.runtime_style + } + } +} + +wrap_browser_view_delegate! { + struct SimpleBrowserViewDelegate { + runtime_style: RuntimeStyle, + } + + impl ViewDelegate {} + + impl BrowserViewDelegate { + fn on_popup_browser_view_created( + &self, + _browser_view: Option<&mut BrowserView>, + popup_browser_view: Option<&mut BrowserView>, + _is_devtools: i32, + ) -> i32 { + // Create a new top-level Window for the popup. It will show itself after + // creation. + let mut window_delegate = SimpleWindowDelegate::new( + RefCell::new(popup_browser_view.cloned()), + self.runtime_style, + ShowState::NORMAL, + ); + window_create_top_level(Some(&mut window_delegate)); + + // We created the Window. + 1 + } + + fn browser_runtime_style(&self) -> RuntimeStyle { + self.runtime_style + } + } +} + +wrap_app! { + pub struct SimpleApp; + + impl App { + fn browser_process_handler(&self) -> Option { + Some(SimpleBrowserProcessHandler::new(RefCell::new(None))) + } + } +} + +wrap_browser_process_handler! { + struct SimpleBrowserProcessHandler { + client: RefCell>, + } + + impl BrowserProcessHandler { + fn on_context_initialized(&self) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + // Check if Alloy style will be used. + let command_line = command_line_get_global().expect("Failed to get command line"); + let use_alloy_style = + command_line.has_switch(Some(&CefString::from("use-alloy-style"))) != 0; + let runtime_style = if use_alloy_style { + RuntimeStyle::ALLOY + } else { + RuntimeStyle::DEFAULT + }; + + { + // SimpleHandler implements browser-level callbacks. + let mut client = self.client.borrow_mut(); + *client = Some(SimpleHandlerClient::new(SimpleHandler::new( + use_alloy_style, + ))); + } + + // Specify CEF browser settings here. + let settings = BrowserSettings::default(); + + // Check if a "--url=" value was provided via the command-line. If so, use + // that instead of the default URL. + let url = CefString::from(&command_line.switch_value(Some(&CefString::from("url")))) + .to_string(); + let url = if url.is_empty() { + "https://www.google.com/" + } else { + url.as_str() + }; + let url = CefString::from(url); + + // Views is enabled by default (add `--use-native` to disable). + let use_views = command_line.has_switch(Some(&CefString::from("use-native"))) != 0; + + // If using Views create the browser using the Views framework, otherwise + // create the browser using the native platform framework. + if use_views { + // Create the BrowserView. + let mut client = self.default_client(); + let mut delegate = SimpleBrowserViewDelegate::new(runtime_style); + let browser_view = browser_view_create( + client.as_mut(), + Some(&url), + Some(&settings), + None, + None, + Some(&mut delegate), + ); + + // Optionally configure the initial show state. + let initial_show_state = CefString::from( + &command_line.switch_value(Some(&CefString::from("initial-show-state"))), + ) + .to_string(); + let initial_show_state = match initial_show_state.as_str() { + "minimized" => ShowState::MINIMIZED, + "maximized" => ShowState::MAXIMIZED, + // Hidden show state is only supported on MacOS. + #[cfg(target_os = "macos")] + "hidden" => ShowState::HIDDEN, + _ => ShowState::NORMAL, + }; + + // Create the Window. It will show itself after creation. + let mut delegate = SimpleWindowDelegate::new( + RefCell::new(browser_view), + runtime_style, + initial_show_state, + ); + window_create_top_level(Some(&mut delegate)); + } else { + // Information used when creating the native window. + let window_info = WindowInfo { + runtime_style, + ..Default::default() + }; + + #[cfg(target_os = "windows")] + let window_info = window_info.set_as_popup(Default::default(), "cefsimple"); + + let mut client = self.default_client(); + browser_host_create_browser( + Some(&window_info), + client.as_mut(), + Some(&url), + Some(&settings), + None, + None, + ); + } + } + + fn default_client(&self) -> Option { + self.client.borrow().clone() + } + } +} diff --git a/examples/cefsimple/src/shared/simple_handler/linux.rs b/examples/cefsimple/src/shared/simple_handler/linux.rs new file mode 100644 index 0000000..0bd212b --- /dev/null +++ b/examples/cefsimple/src/shared/simple_handler/linux.rs @@ -0,0 +1,76 @@ +use cef::*; + +fn window_from_browser(browser: Option<&mut Browser>) -> Option { + let window = browser?.host()?.window_handle(); + if window == 0 { None } else { Some(window) } +} + +pub fn platform_title_change(browser: Option<&mut Browser>, _title: Option<&CefString>) { + // Retrieve the X11 display shared with Chromium. + let display = get_xdisplay(); + if display.is_null() { + return; + } + + // Retrieve the X11 window handle for the browser. + let Some(_window) = window_from_browser(browser) else { + return; + }; + + #[cfg(feature = "linux-x11")] + unsafe { + use std::ffi::{CString, c_char}; + use x11_dl::xlib::*; + + // Load the Xlib library dynamically. + let Ok(xlib) = Xlib::open() else { + return; + }; + + // Retrieve the atoms required by the below XChangeProperty call. + let Ok(names) = ["_NET_WM_NAME", "UTF8_STRING"] + .into_iter() + .map(CString::new) + .collect::, _>>() + else { + return; + }; + let mut names: Vec<_> = names + .iter() + .map(|name| name.as_ptr() as *mut c_char) + .collect(); + let mut atoms = [0; 2]; + let result = (xlib.XInternAtoms)( + display as *mut _, + names.as_mut_ptr(), + 2, + 0, + atoms.as_mut_ptr(), + ); + if result == 0 { + return; + } + + // Set the window title. + let Ok(title) = CString::new(_title.map(CefString::to_string).unwrap_or_default()) else { + return; + }; + let title = title.as_c_str(); + (xlib.XChangeProperty)( + display as *mut _, + _window, + atoms[0], + atoms[1], + 8, + PropModeReplace, + title.as_ptr() as *const _, + title.count_bytes() as i32, + ); + + // TODO(erg): This is technically wrong. So XStoreName and friends expect + // this in Host Portable Character Encoding instead of UTF-8, which I believe + // is Compound Text. This shouldn't matter 90% of the time since this is the + // fallback to the UTF8 property above. + (xlib.XStoreName)(display as *mut _, _window, title.as_ptr()); + } +} diff --git a/examples/cefsimple/src/shared/simple_handler/mac.rs b/examples/cefsimple/src/shared/simple_handler/mac.rs new file mode 100644 index 0000000..7273980 --- /dev/null +++ b/examples/cefsimple/src/shared/simple_handler/mac.rs @@ -0,0 +1,29 @@ +use cef::*; +use objc2::{Message, rc::Retained}; +use objc2_app_kit::{NSView, NSWindow}; +use objc2_foundation::NSString; + +fn window_from_browser(browser: Option<&mut Browser>) -> Option> { + let view_ptr = browser?.host()?.window_handle().cast::(); + let view = unsafe { view_ptr.as_ref()? }; + let view = view.retain(); + view.window() +} + +pub fn platform_title_change(browser: Option<&mut Browser>, title: Option<&CefString>) { + let Some(window) = window_from_browser(browser) else { + return; + }; + + let title = title.map(CefString::to_string).unwrap_or_default(); + let title = NSString::from_str(&title); + window.setTitle(&title); +} + +pub fn platform_show_window(browser: Option<&mut Browser>) { + let Some(window) = window_from_browser(browser) else { + return; + }; + + window.makeKeyAndOrderFront(Some(&window)); +} diff --git a/examples/cefsimple/src/shared/simple_handler/mod.rs b/examples/cefsimple/src/shared/simple_handler/mod.rs new file mode 100644 index 0000000..d61ddea --- /dev/null +++ b/examples/cefsimple/src/shared/simple_handler/mod.rs @@ -0,0 +1,326 @@ +use cef::*; +use std::sync::{Arc, Mutex, OnceLock, Weak}; + +fn get_data_uri(data: &[u8], mime_type: &str) -> String { + let data = CefString::from(&base64_encode(Some(data))); + let uri = CefString::from(&uriencode(Some(&data), 0)).to_string(); + format!("data:{mime_type};base64,{uri}") +} + +#[cfg(target_os = "macos")] +mod mac; +#[cfg(target_os = "macos")] +use mac::*; + +#[cfg(target_os = "windows")] +mod win; +#[cfg(target_os = "windows")] +use win::*; + +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "linux")] +use linux::*; + +#[cfg(not(target_os = "macos"))] +fn platform_show_window(_browser: Option<&mut Browser>) { + todo!("Implement platform_show_window for non-macOS platforms"); +} + +static SIMPLE_HANDLER_INSTANCE: OnceLock>> = OnceLock::new(); + +pub struct SimpleHandler { + is_alloy_style: bool, + browser_list: Vec, + is_closing: bool, + weak_self: Weak>, +} + +impl SimpleHandler { + pub fn instance() -> Option>> { + SIMPLE_HANDLER_INSTANCE + .get() + .and_then(|weak| weak.upgrade()) + } + + pub fn new(is_alloy_style: bool) -> Arc> { + Arc::new_cyclic(|weak| { + if let Err(instance) = SIMPLE_HANDLER_INSTANCE.set(weak.clone()) { + assert_eq!(instance.strong_count(), 0, "Replacing a viable instance"); + } + + Mutex::new(Self { + is_alloy_style, + browser_list: Vec::new(), + is_closing: false, + weak_self: weak.clone(), + }) + }) + } + + fn on_title_change(&mut self, browser: Option<&mut Browser>, title: Option<&CefString>) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + let mut browser = browser.cloned(); + if let Some(browser_view) = browser_view_get_for_browser(browser.as_mut()) { + if let Some(window) = browser_view.window() { + window.set_title(title); + } + } else if self.is_alloy_style { + platform_title_change(browser.as_mut(), title); + } + } + + fn on_after_created(&mut self, browser: Option<&mut Browser>) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + let browser = browser.cloned().expect("Browser is None"); + + // Sanity-check the configured runtime style. + assert_eq!( + browser.host().expect("BrowserHost is None").runtime_style(), + if self.is_alloy_style { + RuntimeStyle::ALLOY + } else { + RuntimeStyle::CHROME + } + ); + + // Add to the list of existing browsers. + self.browser_list.push(browser); + } + + fn do_close(&mut self, _browser: Option<&mut Browser>) -> bool { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + // Closing the main window requires special handling. See the DoClose() + // documentation in the CEF header for a detailed destription of this + // process. + if self.browser_list.len() == 1 { + // Set a flag to indicate that the window close should be allowed. + self.is_closing = true; + } + + // Allow the close. For windowed browsers this will result in the OS close + // event being sent. + false + } + + fn on_before_close(&mut self, browser: Option<&mut Browser>) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + // Remove from the list of existing browsers. + let mut browser = browser.cloned().expect("Browser is None"); + if let Some(index) = self + .browser_list + .iter() + .position(move |elem| elem.is_same(Some(&mut browser)) != 0) + { + self.browser_list.remove(index); + } + + if self.browser_list.is_empty() { + // All browser windows have closed. Quit the application message loop. + quit_message_loop(); + } + } + + fn on_load_error( + &mut self, + _browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + error_code: Errorcode, + error_text: Option<&CefString>, + failed_url: Option<&CefString>, + ) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + // Allow Chrome to show the error page. + if !self.is_alloy_style { + return; + } + + // Don't display an error for downloaded files. + let error_code = sys::cef_errorcode_t::from(error_code); + if error_code == sys::cef_errorcode_t::ERR_ABORTED { + return; + } + let error_code = error_code as i32; + + let frame = frame.expect("Frame is None"); + + // Display a load error message using a data: URI. + let error_text = error_text.map(CefString::to_string).unwrap_or_default(); + let failed_url = failed_url.map(CefString::to_string).unwrap_or_default(); + let data = format!( + r#" + + +

Failed to load URL {failed_url} with error {error_text} ({error_code}).

+ + + "# + ); + + let uri = get_data_uri(data.as_bytes(), "text/html"); + let uri = CefString::from(uri.as_str()); + frame.load_url(Some(&uri)); + } + + pub fn show_main_window(&mut self) { + let thread_id = ThreadId::UI; + if currently_on(thread_id) == 0 { + // Execute on the UI thread. + let this = self + .weak_self + .upgrade() + .expect("Weak reference to SimpleHandler is None"); + let mut task = ShowMainWindow::new(this); + post_task(thread_id, Some(&mut task)); + return; + } + + let Some(mut main_browser) = self.browser_list.first().cloned() else { + return; + }; + + if let Some(browser_view) = browser_view_get_for_browser(Some(&mut main_browser)) { + // Show the window using the Views framework. + if let Some(window) = browser_view.window() { + window.show(); + } + } else if self.is_alloy_style { + platform_show_window(Some(&mut main_browser)); + } + } + + pub fn close_all_browsers(&mut self, force_close: bool) { + let thread_id = ThreadId::UI; + if currently_on(thread_id) == 0 { + // Execute on the UI thread. + let this = self + .weak_self + .upgrade() + .expect("Weak reference to SimpleHandler is None"); + let mut task = CloseAllBrowsers::new(this, force_close); + post_task(thread_id, Some(&mut task)); + return; + } + + for browser in self.browser_list.iter() { + let browser_host = browser.host().expect("BrowserHost is None"); + browser_host.close_browser(force_close.into()); + } + } + + pub fn is_closing(&self) -> bool { + self.is_closing + } +} + +wrap_client! { + pub struct SimpleHandlerClient { + inner: Arc>, + } + + impl Client { + fn display_handler(&self) -> Option { + Some(SimpleHandlerDisplayHandler::new(self.inner.clone())) + } + + fn life_span_handler(&self) -> Option { + Some(SimpleHandlerLifeSpanHandler::new(self.inner.clone())) + } + + fn load_handler(&self) -> Option { + Some(SimpleHandlerLoadHandler::new(self.inner.clone())) + } + } +} + +wrap_display_handler! { + struct SimpleHandlerDisplayHandler { + inner: Arc>, + } + + impl DisplayHandler { + fn on_title_change(&self, browser: Option<&mut Browser>, title: Option<&CefString>) { + let mut inner = self.inner.lock().expect("Failed to lock inner"); + inner.on_title_change(browser, title); + } + } +} + +wrap_life_span_handler! { + struct SimpleHandlerLifeSpanHandler { + inner: Arc>, + } + + impl LifeSpanHandler { + fn on_after_created(&self, browser: Option<&mut Browser>) { + let mut inner = self.inner.lock().expect("Failed to lock inner"); + inner.on_after_created(browser); + } + + fn do_close(&self, browser: Option<&mut Browser>) -> i32 { + let mut inner = self.inner.lock().expect("Failed to lock inner"); + inner.do_close(browser).into() + } + + fn on_before_close(&self, browser: Option<&mut Browser>) { + let mut inner = self.inner.lock().expect("Failed to lock inner"); + inner.on_before_close(browser); + } + } +} + +wrap_load_handler! { + struct SimpleHandlerLoadHandler { + inner: Arc>, + } + + impl LoadHandler { + fn on_load_error( + &self, + browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + error_code: Errorcode, + error_text: Option<&CefString>, + failed_url: Option<&CefString>, + ) { + let mut inner = self.inner.lock().expect("Failed to lock inner"); + inner.on_load_error(browser, frame, error_code, error_text, failed_url); + } + } +} + +wrap_task! { + struct ShowMainWindow { + inner: Arc>, + } + + impl Task { + fn execute(&self) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + let mut inner = self.inner.lock().expect("Failed to lock inner"); + inner.show_main_window(); + } + } +} + +wrap_task! { + struct CloseAllBrowsers { + inner: Arc>, + force_close: bool, + } + + impl Task { + fn execute(&self) { + debug_assert_ne!(currently_on(ThreadId::UI), 0); + + let mut inner = self.inner.lock().expect("Failed to lock inner"); + inner.close_all_browsers(self.force_close); + } + } +} diff --git a/examples/cefsimple/src/shared/simple_handler/win.rs b/examples/cefsimple/src/shared/simple_handler/win.rs new file mode 100644 index 0000000..3db2ac5 --- /dev/null +++ b/examples/cefsimple/src/shared/simple_handler/win.rs @@ -0,0 +1,18 @@ +use cef::*; +use std::iter; +use windows_sys::Win32::{Foundation::HWND, UI::WindowsAndMessaging::*}; + +fn window_from_browser(browser: Option<&mut Browser>) -> Option { + let window = browser?.host()?.window_handle().0; + Some(window.cast()) +} + +pub fn platform_title_change(browser: Option<&mut Browser>, title: Option<&CefString>) { + let Some(window) = window_from_browser(browser) else { + return; + }; + + let title = title.map(CefString::to_string).unwrap_or_default(); + let title: Vec<_> = title.encode_utf16().chain(iter::once(0)).collect(); + unsafe { SetWindowTextW(window, title.as_ptr()) }; +} diff --git a/examples/cefsimple/src/win.rs b/examples/cefsimple/src/win.rs new file mode 100644 index 0000000..b28debd --- /dev/null +++ b/examples/cefsimple/src/win.rs @@ -0,0 +1,21 @@ +use crate::shared; +use cef::*; + +#[unsafe(no_mangle)] +unsafe extern "C" fn RunWinMain( + instance: sys::HINSTANCE, + _command_line: *const u8, + _command_show: i32, + sandbox_info: *mut u8, +) -> i32 { + let _library = shared::load_cef(); + + let main_args = MainArgs { instance }; + let args = args::Args::from(main_args); + let Some(cmd_line) = args.as_cmd_line() else { + return 1; + }; + + shared::run_main(args.as_main_args(), &cmd_line, sandbox_info); + 0 +} diff --git a/examples/osr/Cargo.toml b/examples/osr/Cargo.toml index 77c4f2e..86c3315 100644 --- a/examples/osr/Cargo.toml +++ b/examples/osr/Cargo.toml @@ -7,6 +7,9 @@ publish = false default = ["accelerated_osr"] accelerated_osr = ["cef/accelerated_osr"] +[package.metadata.cef.bundle] +helper_name = "cefsimple_helper" + [dependencies] cef.workspace = true wgpu.workspace = true diff --git a/examples/osr/src/webrender.rs b/examples/osr/src/webrender.rs index b39f9c3..d3ee695 100644 --- a/examples/osr/src/webrender.rs +++ b/examples/osr/src/webrender.rs @@ -33,8 +33,10 @@ wrap_app! { command_line.append_switch(Some(&"hide-crash-restore-bubble".into())); command_line.append_switch(Some(&"use-mock-keychain".into())); command_line.append_switch(Some(&"enable-logging=stderr".into())); - command_line - .append_switch_with_value(Some(&"remote-debugging-port".into()), Some(&"9229".into())); + command_line.append_switch_with_value( + Some(&"remote-debugging-port".into()), + Some(&"9229".into()), + ); } fn browser_process_handler(&self) -> Option { @@ -223,7 +225,9 @@ wrap_render_handler! { ty: wgpu::BindingType::Texture { multisampled: false, view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, + sample_type: wgpu::TextureSampleType::Float { + filterable: true, + }, }, count: None, }, @@ -348,7 +352,9 @@ wrap_render_handler! { ty: wgpu::BindingType::Texture { multisampled: false, view_dimension: wgpu::TextureViewDimension::D2, - sample_type: wgpu::TextureSampleType::Float { filterable: true }, + sample_type: wgpu::TextureSampleType::Float { + filterable: true, + }, }, count: None, }, diff --git a/examples/tests_shared/Cargo.toml b/examples/tests_shared/Cargo.toml new file mode 100644 index 0000000..27b8dff --- /dev/null +++ b/examples/tests_shared/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "tests_shared" +publish = false + +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true + +[features] +sandbox = ["cef/sandbox"] + +[dependencies] +cef.workspace = true + +[target.'cfg(target_os = "windows")'.dependencies.windows-sys] +workspace = true +features = [ + "Win32_System_Performance", + "Win32_System_SystemServices", + "Win32_UI_WindowsAndMessaging", + "Win32_Foundation", + "Win32_Graphics_Gdi", + "Win32_UI_Input_KeyboardAndMouse", +] + +[target.'cfg(target_os = "macos")'.dependencies] +objc2.workspace = true +objc2-app-kit = { workspace = true, features = ["NSApplication", "NSResponder"] } +objc2-foundation.workspace = true + +[target.'cfg(target_os = "linux")'.dependencies] +glib.workspace = true diff --git a/examples/tests_shared/resources/osr_test.html b/examples/tests_shared/resources/osr_test.html new file mode 100644 index 0000000..629bce5 --- /dev/null +++ b/examples/tests_shared/resources/osr_test.html @@ -0,0 +1,205 @@ + + OSR Test + + + +

+ OSR Testing h1 - Focus and blur + + this page and will get this red black +

+
    +
  1. OnPaint should be called each time a page loads
  2. +
  3. Move mouse + to require an OnCursorChange call
  4. +
  5. Hover will color this with + red. Will trigger OnPaint once on enter and once on leave
  6. +
  7. Right clicking will show contextual menu and will request + GetScreenPoint
  8. +
  9. IsWindowRenderingDisabled should be true
  10. +
  11. WasResized should trigger full repaint if size changes. +
  12. +
  13. Invalidate should trigger OnPaint once
  14. +
  15. Click and write here with SendKeyEvent to trigger repaints: +
  16. +
  17. Click here with SendMouseClickEvent to navigate: +
  18. +
  19. Mouse over this element will + trigger show a tooltip
  20. +
  21. SELECTED_TEXT_RANGE
  22. +
  23. +
  24. Long touch press should trigger quick menu
  25. +
+ +
+ Drag here +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + diff --git a/examples/tests_shared/resources/pdf.html b/examples/tests_shared/resources/pdf.html new file mode 100644 index 0000000..619b1ce --- /dev/null +++ b/examples/tests_shared/resources/pdf.html @@ -0,0 +1,9 @@ + + +PDF Test + + + + + + diff --git a/examples/tests_shared/resources/pdf.pdf b/examples/tests_shared/resources/pdf.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1e0dad53c145ade82eaae060fe8b951e02ab761a GIT binary patch literal 30110 zcma&N1B@?Gw>{XlZQizR-L`exwr#unwr$(CZQK5}ZFkT8-n>jE^WG%?q-tkpok~^K zs$DB}PR=G%5D}weqGyF68!Q^^8f+QNg<&RQB(gKKgyH37kTJ0}b2cYp`G+Vmh*?-W zn>aFvSsOT;h?p4J8JqC)!#FuRni$x?xMwA&$Xeks!G+#@qw_<$QjaGH=wq306wPh6 zfbu2fZ2VH_2mvJY>aAbT*mfWf)AAU_(tdcDNV72J`?W|8364KF%DjO{eHUC;K?m3P-Cy3${QG7&(6x!}UhLk12b0i`_ zi)JL(l)^)$OCJ%5G`)yB^_39A!-5o5#zCt-<003a9EdZYagbr9z(Ji(7!_f8NHa&^ zq{LawGl%-YU`$M5ou0_@nL-x00@pKl6J49m@td)@p10lOUD z!kE|^|M#o^m;I+&VE$7i|4CT6*#9&7Ul`l}C?)p)gHm#dJ^1&=Z2EZIV|szoyDE-}+Q zSCN9X#Kkh+Ku?@`?@;{O4~{Cq8VGI?3)Agr2*c4tEqZ{)ToyYklNlqnTPCmEhcs>m zzroz_Da3Qg)M2~IT-hA&szy(>*I{Wd>0$neJwRN~quSj!^e_W`0FyKSrB@mNoI@Zg zwO}#bUhdljCH4ifEtAoCR)BeN$0*dF!H!%A z5Rs`IL`GVW2L6f_iKb>adLb|0l{+Bw~K{-ovv&>w+RW@z{KH;|Mt)z9BuAzBG zH{pKcU>g_P`b(}gFKPB;@M&RNPq%9zyxxfMpiLX};DP{m3r6@Q6#Tzv@gJ7|UoDva zr^hh;|C!8xn-CKb3lk$N1H`6>0GdI&vwwns5rmz?py6( zo~zr6T&aGaiU!wp`&{HXk9Kl&L*7aTH&w1YRqr-kB%5Ip7tx4Hybix#UtMS})KG~? z1d}i7UBQ9#G%Xja2GhT}QKl_IgI`e8pdx7sA~SPy!0YzR8LNZ7k&LyvytF{>zCAZp z694dxVOqoJ?zAd(CWU?05;Hu3afD>`dc2v8fx@#`zI#GK+=#U!2eVHXKlyy+_rT_w z#R~Rd6CZXegzo1PqWWuQP~g?h!*F-^n=@-w6-*Og=Ej{0{>*x_LMoyJ8&4ZihQE+d z{6^kW2JmIbIg9QTKCzM!0^PTvAZ3Gn?}BeE;Z z6OU0Hn2@Q+$BRSxLm(FsmGAwCG`1jo%DGn~qE;A0iOfTKQ)o38y)`*^E@mvYsDND| z+k=_ms!ai)InCMbOT25B1K(-S@@q2#l@BY#Daq4j`TG71tGC z%pcY{)qN!IQSL>2Ge{!t`{Y=Py*||Rz|$an!=scC-}HwdP$@iorMwfmA%3KHJ)miK zJUtSRw+CS9w=2+`i@f9qxwGs3USO*vU5-DbL>_6w^X$fqF8I2QD$LDat&?;jzn1{O zN&WUHLpf#%^^J^mM8&MhR+qqr*Paj}Q&^1jjqwf99U%CGU8rQTfMNBWMvqrh;LQk8 z1!|5JnsmD@1z5V*?B^U}ienJN-X8HJZJwr{)^cK>9BOWfI7%?I=U#|&V>w{(;+Mg` z`XD}jLp<+)4T;Fv3moJEdgn^yvZB=WMY~QRPj}}S>ab0z_kq3febIlSlM>kgkN~=C zug}BJn-HR@cP(?&4tTzB{$W>MOoq|aeK~Kko$%hEo8i$r=~q4_F`RL+d#2TKn`7S* zhQQJQ!oAwn%ek!rAG-pNgSDX>a{g#E5!bvUBE(gYC%60$15?O3=tm0o9T7VNcy>cr zUKF`en%ma>cAVW%!jaCqLw7ha59r>I-O=2=-a`hcKyhg$3GQ@=*s#Y$8-N!e3;@Q< z5RW)+mKBsa^47MlO|?4k1fs&zu5){N_&q`KK=BH7+b-zTdJH^mJDr$e#%k6?zR!)n z(Cz*$j_JtI#S}npo6+WF=xj)Pua*R4l?JD6U#OJF4>b#+e$y#*@SK-g5AB{B<#Y$q z1;^P4Vb_;=chonId+YN$8ugreFxXcta1a|{%HqJ3VHUS;j9eaaqV-FJJy8cB$1J8x zmg@v!2B^{oNu8wI*K9}P7_&LKJ|5VL+|p}nZFAc4a3|>saC=yEXSg{`y|sF!`~3Rs z{OrtF&cI}~vx47{-N4^)xblN>3I_k3U8il1evR}DTYI`8$ek~Y<{{K%_!lK?zmhwm6OdwOOg2A^dus>H-0JQq=90%cxDbI09AcVhD006DKPua@^uSz^0tdtCo!WwP)%45z>zWUNzY=PLITS$@ zm8E^)b`aS7`iYSnr#l6+6ST7z06v$G%XM7-#Fpi|4#u}f-w0qAuMyp3oBq7H z^BmeIPa4k!yK*H{>nj!Zgf~xK&RcvQyyl1U%!{fFMWK+GVUCML&m+DLIw6=t<&{vt zyu3@!8}^MHX^J++t6o)SG{+Bq#UwwUuNW&l-5K*-LTyKth4iUoaU^?e!|jf~7VNx> zM%@+jp@esx!F5kJW#Y=$z)P149nX~jZTC8V2We%gNA&@kl_zc#bb^9^@DqVIHvp`R z5ejxR3JvZ=t`tbmQ>7h=GY%+SQ&MM@5tGIbYZFer$Tsobs}DuPm$58FTh1Fi8VLmj z1|qI-M)@H=e6hpCzmE}NW}JfXZvkuHg0{B!f48UaPU+41g~s_gJWQ0JN1J?yz|vD? z3)C9h-dz?91ZuE6n90mO932~*d9bkXE-sL{X;#pXagCoz$o!3M+!U!Cwr0k83=O273wJOF~-P;7>yMLgaZu2!|Ox3Ru9DD&k6D%2DOkLP-fpyN`fSJ|3?94 zK--jP4Y@{Z2d_**Qr<2-2Xo};DyP{1pn=x%js-()HW=J|WDo#P>N3Bcr&Sc+RXErh z6KQUfIlHuf232E=;M9-79Xbtps2Yr~mgOjCn(yT0@i4Qtfo99OcbQ=B>h4~O=4tY;UM5(fm9xB{_zw%1_R9aZdIP)Ma zkTt#hJ6r)o?nH|evBLB>U%9M;e?mis>i$olU~sW3G^NFJU)J#gB`YR;>nu3yYm@%J zZ{8pM4CZ2TGj!OaDXRt1idi(4_0tUuWsmD?e<>kPsbCuICx%ku=ohu6LhXP-E<;wP zw5SMW7#lbaX5HuInTOVB_~;^qR2Sv6x0E3yT?&#@)2qZ&?VqxKl$@lIjT6RppQczX2pJugOPj-2m3Aid+x^O)#508gOFn^fb{(y!Axq-!}y@qqF~J34Un$$p$C0sq$cyB zAUjR|gHaB)DocJKGnTWGkz>U`NsrBflz6x@D}JH?U$f%il!f0J@#YXJ25A7wd8W@K zP~YHCb-Gy?^&THQQq>aWNk&UT3deLxM1TqCTmOR<;mHwBZ^X24x*R=jLVY8;HU84y zreh+I%>eb{&-{t~ao<%96?j65o>h;I6h ztAauwE&OUivBJJOZtb?OfV}$#$!yto1NPJzsu8BkhPnKkasySed z$jXo>prkEfs{9(TBVs&VRwy}P5KUIGTdHbCx$CD5;0p}t z*BDsQ=PR8US`?5Qkk*?|NCE0aM(}WmB~YErIDWzidCMO~mS@L%pR-FDVGO>YQH>NU z&X3KKj&-||wm#+mpiofU2bQD&TYl=H)aI zwzldqo>hD)2}@kBl;wTZ*d@Cyqg_)~BqCQUZY`spLATUa0#jUj5H9Z82;%Z75-xThV$Gf7gS^$D!0hayx{t;hRwQl_$mlxcy7r=}|T z%+fE*f3ERGP7R*XboSoVB!WQ4+kM+KYsz2|lJ~26&D1RkkeiZ=_VYLe7r9<0=~i4) z+-DmkTRNPMy_g3l!e!#wkPEYcKhF8)Op*~({%g}B{4bd{q^=U-PDrymUf5U3nT3+} zRsp)RKG9Lpu8dO>T3UbcJ2R6bhHGm$T@nAiq_g5vMct*{f|nw~40fzHqiKB{I+ruQ z7`M8dqfmz=a;~#WYthA&bL*uY2!TPZ5AQv(m5G`gG|9&kykymsPphi(?WKi&Dx&WYY;}(^E4MV4iIc)ivObWO>9k07CIuTv%=9Z#aN7Z^ zk~*W=%q59k3J2-rMYayBnVf-;VS(_iC*NWd^j(a|PCS$ub@d7h-C*at#Y2Xk-tvct z@b>u|>h@&kir&}wi|^w)!nDwZa%>nOb$aB_60h@c48!-88tRV=!*}7D)8>RID0;vX zf%u%-XQ9Fk19yjR*NWrh)vB(lmUh58{D?z-bX|UgFePIJlqYSPN6m zpKz7C)bP~d2QlT0WkVqZ3Kc~Ov#{dyzx2_3rWTjy3zs$xUM^uICjdN3%Its!bqbv5 z4Rr1fIag}84r40pLHUNoDr%0#4(;PIN0y(~30YkiE?YQv#jKKZC1#6Qv8KV9f|klkM9D`< z1DdF^Nbxu6kholGoU_d9iU)ImU^8HIt?H+KM8pg5Rr`MPrq_WCI~7Fw3wSv$FVh8q zvZ;BWyhfvvwgGqqa?s1Zech&&Q5h@|$cyN;cpMJaj8IWyAz^beSZHp)3vawLG9jFH z;S+pGtHKDrh6JKDc6q??*!uLn{V0*gB_tZ|e&!B*B`GO-NRl8X=4$I(OA8Lx-c7VR zGe0KZS|wk9<2N@xb_T>*nw2EMCPf@&7?;ZL zwz6RRpL<=Nm*nn}?loPnQk5OUKUig)TVHT*d~t*aDQU|pCnP6@2sjbmdEDj&sxAOv1%;>h*+QJD|G4am zB%Bvqs4a>x5L|K><@B$Lz9yKwqe8<<$cjJp&3#Ep?5fIWgEc7)`2|=5ze0hfmecR3 zc~^`xF@l)RMD7#iie#16j58rv8Y#={M9fCy2S|gtjUg%7d~tQ`g(g$7hePkiXpG2# z++8v9<7An~(Zxq;Nh&$dXG`OzXYV0!!{Q}r%N6Zz#b3*;ND+=r~9Jqtz6FctAI8QPqwM*c`cOrSZs z@Jl0S&`m8)xBju0Zt~67R5PJ%rLWhq?48oI+og0qUD<54szriufoy`T{>N@@iFgx&O2KDtlSVd+dbTE?6DH4 z{L;Q*wxk*+$B3kDsau;c>^vYV#iCW+iLvGuN~Ka~p?j7IrUfw@DUo?_2dMZlEfbn1 zgga6BPjm9iSut*OLF87)*K$;~xNO!m!EeH`gU%Amqzdb%NvbNBwio@{bz|3ag&9_U zjL)`wmAo5u!hxTM2^6zqJnd7!63LwACMe&i6mqrTRXWo%tJ5qpvk8<#v3Bj0Cmph4 z4Fq4|TC@)5$fV=WTs~(xJ;>);K*~cOHu6*s1m~%3+>pUwA)U2lPd|N2K&ooiD#tYY zD?pLqkl8-VYkVx2LHK}MqzQV>rfozN>d-JyTid*^yUE6GSjbga)^C%+KOG0*%;;-P z$dBn}i_fAZxooDTm5r&%F(~i20PepF{3U`o=qN*&#(fC(xu)LL=oxBvqB80_WADob zT)t(291D$6p9BH3sDSRu5iIGkrq^2GF-4pH%aPL&#d<+^;FSdR6#o+ zkJsCI$lOWX+DQul&w zBb$Tct@k528sZScHSB;S9L%SU5mrf#fbZ>1toQmF0BNe%;5R$8mOtwBJ?6#mxtDb1 zvEa0ygBg?PqSqx>v*q>;)@%<+yb&HlieU1dU4(`$afY)LStI0y^h2~HFec!P*eqII z{n!QCV&-Zv(0@1@8&s7qiaObj_zF%5e{|sXWg}Kd&Oe)m-8+T#clBI3QRyLS=apAJ z+(GWMcx3oa_IG3b;%oc<4Ri{2PFEFp4!Fi-gXYCs8R=H&dy! zxxC%1#U$>87u`koV=OPB**aeMMz8}jbIMf&>Mf_o0E3h{xuvj=ITXGq12J%PV&!MS z$Wa3h$f7p6u;Fh5(|q8p3(2qzpt_0*_;bbK+jCazFCZ-O%829j8h zO$46iU4(Ok%`#v&^RxW<__6anz!`@UzEFJ=>kl%^5rSuc2wP0y6q3;^9?+ywMh0Gx zYF2njyNcGy3RTH)HRnYPx!VZx1@uMc%HL!+Elf?T>MRw$B~8)LtA7s#Hw&w(BUc91 z&lW3h=%5r;MP2k7Pr|cGV6bECE}MZqQCr>w^H7$nKL}9|x7^mAfRW1vojrXl4=H76 z$|<1YLA41ztP1J?ge;-Y29i>x>llZDRvNkHN8>AYJKQy)WI1zo*K8WmQWAEZlb3?q z!T-F;%--1Ycn=rm3kbYEAYS`CR?xT!xC|e4W+tYZVK`S&BkTrB|2QI!wC=Dt`wj}o zX@CDp{pc(d*Y$pGH!IIl5!pFx6r7o&gzl{u=$9Ta z+T}EcLmal~&`FtXZ3$Fn=S(;ROolc8$~)yED*j_%5`Jn~LuGF9X68s!7%b=lBIqo8i6_-(4(P@mnSQD%I=Kh5 zX|T0|h&WMn)JR&L?*{U^5pvikfvYK4CZ^d%QAr_;V>+$0LyH2#D3o-b^RWDFbK<() zqQ}T3-?RKso#H%=ana$3QXH3ZpFcH2)BK#;t#TOe0G0!b3gO#h)h6u6e7)T~i@Dn6 zs^y1@iAdB)p`1AH{Q$$_@`LzHzc;h2*Q=klqv`NPNHMo-tZlEu8AX(^M%!KPyH(iH zpF{4f&}oLA+k+e5w}FR0jXH>mOgyMhS7f1OegsY++=Tx*T?C>li z=GAfo)p~Jnx#n&;NPO9f|Lcjxb;H3}7OJO&>EuzMUaF)rDnrT0DuI`L$5i6p9k ze!0WC)(uUGPI%9=HI^)>YG0k9`UPY3ceCtlIV`Fp_->49d7JmBwp5`aazx`BOzf9U%`cfKzqO+oN z5JZA3%0Fy^G$(WqRyD0Sg6v9XkjC<4m@x#` zpBt|Qz`%fMuDe?Hp%~+tzp+9|+rZ3n<^#u6rnM%7r7z(|Ntcq8D$I1c4+U@?&A zH_7zrak^wzYX9VxjUCE6TaO+LnJ$jlB;O5IWH5FTaINFBKt;Az(CUiIk>H?^D}7;F zY}#r?IQDVg`wEYB_`LAz!|?v*8$6A^YSnd*rsbHn>3EwMbx1rEH(HD4vL0ARwUhBC zm-0ezJ?L~za^r{TY^J{_aKkD3l@1_;$jMcFNfRy~kTrJg8p*!TGlcamO&9S+}V| zpmuHk*UG+WxS7jW(6?{f%vZ$w*Lo2P1vYyey={Rz-cSH21{8=n9+0- z+TJ=EE7}`nL@09`jp7FSGLLQ?m3`>IXijR@F#&W;b!-qZkq#ci zSZf5J|1NJ!Yjre)vyKh0VXEVDOtBF1ES&}VatG{wEsHrRlKVvn6#rBn+DJY?PIHBG zIwy$V$izS5ok%)apq8al>sM27NwRh&g}a&Xr%5v7#~4c=)8n?>{gK_wTCxKPy3OB} z;Hak4Z9jIOy~)#B5i46-Pg^uxR5}?i(fy`zBS!11_R6j(u&=0Cq0+MLZ68l>hxMjl zSCVorLSsh(2;6U;N>K6QQ-zFkB+1QHO8Cn-PoX{8IKaR)6UP6J<^C(a%|kF;8f$lN ze2|J%qLr(#TY-^;dL`E~L-H^!^0a|z7ZCRN-#MV%@l~un+<(T{*mXV3Fxxy#25((=*qinZysK&_onCl?IU;X zLZdtH#>;3Lf3hZf)a#}~qp{^}FXs!hVXW7sHr;KN6thQOjG?#dy5~mw6_pQJ8-_mw z*42WYn%wU7;nCE`Hi3(aV>ByM%P=QVA&Z%zmAQ3!E2Fc2D|wUMX|0R&q-Timmw>I@ zSjh>*#0o97xFnh<>sk2amt_Q899GQ)XZ{dJ?awE+!;{`$DH*wTKdQb+TAdhl%^jFvs;Gdl7&=1BUU;mY?n zIdavHzF9X11V#q0Y>x)p7FMnYxKL4=EVtoKZIFl=aPS5=aQc}vRJ0%{-W(?;gj)ea z+VFzB&k^#s0Th!GvpHP1qoAML>;=1!R9%h~|?rdb2BZ~vNk-gNQ^TIPbh-2(#3))EA8Qt;&d!PT7hb!YcFMx86OcI=mlA8* zZ%ok~lP`f}>g}Z_qvV`q0cI`#78)wgBKHlSr|1p8Q{tNa>o|tPq4h8D2pCGyX+_w* z{D+cV@z&~2ZW98E8##Jf2rwaRJ50SS*VZ2vraAZ&DbDd15jyd#VxMa0*Or#w>NuX> z(Ia!a>W?=v+#|WTNEhc=z7kTVXZ}s`-hxtR+CHZ+IdqvZf5pY&xr+6Ko#6 zRbb>I^SB}wZc8VID|W~;vK<_zF!^~*8=3Y(H1=yuE+8CB#zV(?kNe!wr3OFuZB2bOi^aT=X{e^SL&9OXLW z*f<=EXmZyS`ydFZIjWwex8;A-T0@@^6$536ixwB>A@}lsV0&l|V-;@-3)VaHJw&0F zr7IDe6jKg6g84vq;Mhdm+?Dg?Ig)8O`+IlBnP7_KqIvaDW4z`y&aK8A7EZrDoW3~Z za6h)AS!a8=)*ibMPHEu9#Drbqz1h!rG$Ylm!N`FPfyV%P_;r>;;{8_}7LpHn_9@f) z10#X0C{cz9@1v+YgILLJ^Ok#0TcNJZ{UUr9fkQWoI!iBk_VUtGF}4oNkPhORxj} zx*zf|JQV*{_`$#9bJJS;LClZz61&^&ZF^=k%iG0$RbDzFwHd4Hb9v>0+xs!N--hb< zzE^%sfgs?hY>y$a(lOSVQJs;hNZnCan{6#YrmnJaIjLnu`70m9iKlHos9g9obV=uw ze>!n1b{evc>7jOtAz!)tC)u)bP1&ZrT)T#SGvy*-OL|K7js`U&ftw9ix!0z5*q6|) z_K1Jg_7iH|<|BvB~RXx*?OY1lJ7 z1*^Zg4CKma#jI)iAHeSxRX*bZb=Quz)U2pP`KAn&Xhbe-boP_fVGT=2*l?tjLBKey&MY z;Yt#}U6KXrQ{0sjt((Ew<7hEZsmGM><_Uyss^W2U4Jg}10A%Ism8&usSmM~;F$&J&V z?j8$}o+zR+gENAwiVO(BQpW|wJ>B$W@R4?3$_4IIK*9ShYYKX-P*C=w0ly()FHm*B z$_k%(b)K=d%vO(Utjt?Io?|h9EQC1+xe?57AhAYvD-jnA8B%Be$6ay`=J7j(K1om( zzEiJwx7*0f>xqvNbPZZhSLMxd+0J;iO7kd2rAGt;omPqHm$m^wl2ENRYJ-De17Dra>Q8bk?AymG3K}&reVjJ_%6~R zUTnWLo5mtsXjU*QW^T&i0w9ggG!d24CUtFJ2&Kze7i~N9hDXltH0Y8H7E|elzRE z#@j+7Z9(F%#jhb&6o{@RteHo{b43(}CpZhQo0_v32VOVUGZSy5S#ZDSvRx>5l!9I1 z-DHlKn?G@o2b%;ksidK&a?;uX|a*C@^@z0H$v2veZ_|G(kk7_SFSSO1D z9wyPvk2f7?_#e~tVLvI!Ja1mcsFTgQb;VF;$-9}RSHa+@Y-Qf}snb1=G{bUqXm@_X zENrr;22hn}H$Ot2y~k>&zLdN0Z=|0<9k}1>pif}mLR|;JH690I1^hz*j-deQ@h8B=oZL)7pAYBwn5u65@+hYKbI9}#6nsR& z=;KcSG>Y{JdYbU!WfsVTgwgCDeR$?|Cv^2lun#ld3&p-3;ehFl zn+-kx9H!a%RSwBrL&l@Hw5EW5O;um;AaK>5pnh(M-D!P3q>PluG7wNi%CaVSHr1WjFD9jR z{RPKff1k^|Hn0~2he2i2KFh?3pzn1}0~ce)dBeyV@bmy3glf?q%g7nt@}z;4_1nRl z9-@>vix;93n>V!|8?HC7za6f|?ADn&yoVj;0_zKgHg@>fBw>=lj4?Cv2I7&E6CT{0 zJR>9m$WkPm63l|_T$B$9V&3)E)k2aB@D5z!fHcvGH3C;gCA@trQARMaBK#r5+UH0t zA?)9cet-}Pjf3{@k5-csKHo+`Y0~p1z+u@TMd)TT|AuZZ7VPW!cdkwu{+_G=Z&d#n7=6bNjJk{QM8QnmE*A+dFk<}2$S)>ee~9SEBbjd6^=bN#HuJZ+s= zsl0H<(#YlugRbn^jW8@RD4Regs!|?xS}L^iHWV5{f0T*do>LgPnV=O!5Oke6N1!%^ z5yrEqQ_UL4jt=Z-y_9GVrnH4rd$J_>R~_r6ol|WmLrvEDbOe#|bWqaSkf+R{ve5L= zm^??;u~*^Z`k?8YnAUg)&+Q+ngr8Eno(K-AVRSerT>WPtjlwDH5>zS5Rfx39mHPO14Oz@gE6}0D1|cp+*kqtum_8iE@B}Qx78d9PHE%}h;R~$l zpeQTW-O=|ou;!rKvHOOUW)(7ee^vjcIyF_+~kMbQM$=` z%1?g?4jtn9@h2jDRS^p05_0(e&MOTJRAnkvfr zW`Ql6SY6c;a(Oy0_|PX3Q=Yshla`Udw&P2jA~0zM)O)=ocm!MG|MAm~rA!U9zV41-S8W!8q*({)iYN13;Fgftb)#`i ztX_D1x|n4BMAHLO<)sJ}cTP5e=`6Vn_iGm@DehF~LH{`DpuUmrkUIKG)l#k*-(Ygx zq1H+C-2lw2qii3Z;~X>P3EJ#y+3hxSi zn}|TH%U-}Q7TVraHaXKfIR`+U(;8ik{7KIN46%=l%Kl8c{BhxVBK1O>k!ddNmUyP4 zcxwP=+%Tf;-xHW0Zy>nQXHmFX?W#3@AbQ-GWSt%Ec3S6Ax*GWv`)8ANyi~oLxp^6A z5%b=&TIJZ+fd=O@L***gQQu$E67fQ3N3ww~x_SObi|TK)i*&zN0amj)6F@Z(Q)O0) zIzP(O^+{n7tR053+CPD|&s^cWaV?_SnB9VEuUh|E?z)iR0B5X~QKuD4l2+bH#a|E9 zXTAk^X=(&Y&<-B1G3QAKzIc)dp3=QZlq;#qpEo3x(S|Zs4YV5YAXaQhXd;E1uN^UC zPpy({Et1?|IkqhPBb~g#eA=1tBz{(U$Y5$On=~^FQ*06?-ek66)_xGS(zt*gw*ssS zirX7Ctx?hvI&HwTH0cD*2y;5dlGHxoo+KbE1+POYwae^IZl*HgUYDfEGT!SbapCW{ z@qex5;Tpd9o(ZBXA*090D4oHe#{;D>Y_F-p%|} z;XGlqG@?KfE56E}f}v7T?AIU;&|P{*pOErPW=Og)iK0lV2|XRtTYZQcEe=d?j3^qJ ziTks?xyaMj&G0Mv<@x3584|M7)5}!CWP4l5XZ3xiAGn;K73q8z=RA!#eOg-B>O*+5 zXro6#p46t1wyoQ3GH+?Sv!_zW^5$xDM}AYH=_5TPA(edCL&>MEvsPIX=hEWFW*4eB z_1X1VLb}x%ZY~~&rxT*+dd+v`=~+6mW5w028M2eiM5OO5M5s7mhwkf&SYid<7WO%; zh>j0G-`65;rm#U#b7h&}92;sn72-EBRg;BHSt02XvCCw!lDK(ZcW-5^TwL@#G7g-p z3*r>z#SM(>YS@b18NzerR@x{GS@EBVnrq9Oo8rRbx`+~mAD}DKnW*;Rs=N+GTOenM z;{I=aadYK5zrv-?>kE%h7xa1Nk^PA8y_TAsC#9GK|o-M#Yk&)q^3tfT+Lq0?&sY*V-j+T zEu!uiAlcv3b6qnBgrDu|bhqtNuXE=GZiGGTo__Br%zz|!Lb!o*9*RHc&h7G}=qtM( z;Oyz*3=fee1KR7)L~P#LBLD?=I+nQdH0TSWVS5AjTpOmLtom$S3JZxZ(QRcw_@vMR zmX45@_S5L0kPrw8+D$a_2aelwv(*}81oEhQV;R0{yo66Ky>=;&G9c9nOBA2D;|Gx3 zMw!!8ZB>_uF)E>wL!y%8+1$|yJETaE`Mk_fDTHMJtK+~u$ZMf&C%(#^5sD@J-qIHg z!ToUB;$O3Biyb@wtXVb&J;g{!&W{Ar4Z}CLtlH+RsH6ePUDU;aCdWe3;___!PT^Fv z&{NzC`B(ATf`L+6Y%)~gBahlJF#(QwXMXs3l~-jd$jYlV)Ub)GjI@ITOVpX;Np>Pa ziER!$*rOU=cD&b*InojA;u)L~7w4$P_d@K=U+T|%A6r#LEx1y>> zOBLI{VIt$+goc{hDn%kPPe+z6Em~?VcFw}6S_JY>M+F?~+)M@N(~&f>R!f=L#B7Psc-9* z?kP$Wcx4V;EBBPl+8$320p3`BG4zHR_89`&j3=P@=5AJ>ltAxPkLM@3>run*gz*fy z-!C?`@l<=nV-$Wxe&dhT`cBA{#ZML@riUuUXFfQzm!U0eDcV$cWlio}xOC`oPaWkY zB0;6HoJ+e9?lrV6&A-|HaGtDN$y3&)$ex_;hEJK*gE=q+^C86u?#<)Gd@m~@A6OB= z39W(dLj|W2nFoxqs?rFZhTH-LCpLvjA88jRXwgm{B;?V3xi%&{<;4<<5ZMcsf{MB1 zX+l{MZ2|8CLurPJOM4P)|K|=YRU@-pVmGv48~BB-b;dWL*B~kI6YI;>w4H7yky)w? zsf!U(*fu#)?3EJqIrS29jrV}191TVh<)HRGa}APIsKg?@%jk`?xruNn2R%rg;s9fU+~G zE96;_6C{AtuKz*pUdtd)EUtIi1`SC zFY7($9()3ZnbS4xkd@A$&5;J>X`4}kW0Rt{>G-+Y1W zBt;}eZwj@?ky`%&INwMC~bb=B6C5R)qMd^LH_9B)hy}5D&5h1;SZ~ae1U;P*DUR3?f z0at54NYP!DD?tAM8&W}}=){lQnK4JqRofw@^ki#LdYk|I5awMMBYeb`xA^Ecx23ng zKMcc*h{P-zUZ-kQx;B<#mRd1y_YL_*qM` z4&MzA%NZ4x-_Hl;1q=(PyHkQ9O17;D$SsJ|z-plAkMA&doDp>!1Dg}lNJ7Sx1WlC& z$#H_*K|>*@Jb)&!%8JQJ6`nP9x5FNNMtOjD*ZJgxgw75y^G5Cj_YH#Z!5Ti&I^WYe z>xZ}#2eTo?{C>sy4Hk$|R|>rER<{Q8UlOhewm}SxA~i*2#S$YeRmQc2A&Z62s6o|+ z^d*|@=l}en&o25<7EL`xO?7l@t7C&-EfHOiV3Lvni?(S!vp!tw`cZxm;AbQCNzd7m zyUwe~yi{7?5fcDij=sTuB5+J4+q)@bCHRGyO2jY|>`afN|F%9>Txfyp7*9b=hvmX1 z#Bx!_G3$e*WnTGpJ*!>1b<;Tl?SIpb(EdXi{ouaz1xe}Q*!zLLI(@whh`q=I)Vbja zJ1Q8KL25f~%k#}D)P_WSh;6Ga8uxxsucEw&VOBzoqwnJQXYHQXywo@{gnUzLTWx;> zpE~t=(e7;9vTLDE~tmmM2tF=3dpxK53#X^EBA@G1#8SG=A0&P$rQ+7@%&(8zU!dO%1Re6$Z=tC84X72!52dX1 zXe%%xC8fe)Yv?vE_y_e?(C|BP|E!NwnfKS(f^pY}ahHraqB}M#FmE@333HcBch3G@ z+Cem&F&MQNGc#5UD1s@RSs(H60dFoeXL>pug##4dtbL9=UIFJmINb0NND?ciaR2z& zu!1&cdOoXc0f2^*LtN3;l+?i;Wt8kNO}>KjIMM&X#Si9~CU=e`7x5;0ccCZa`=yRu zo(L?T&UjH{~1sHT#4{JEiKcW{h~%qVq< z5#1ewfe|tJq02NWcT-uhcXQS?nHcDpm6nv}WPwP%9Wd(7+u2N|GD6^J11`1?ok60R z;u>6lsw_jMu2`kM{5&=67p8+$^27Pt85QK6ZLw6aVWt`KJ-jXUT3}W{4a}Pv5WfOz zTL`muj1Q>ZOF)O<8etU{mPi?O)}{iwZkxBt!(Spn3*2la_Z&@AR)`T&@uLB31m;zle;#0T^j} zK+pTY&4?k^Z}P_r!8020L0V`M`@?w&6Lk6G{u2qOVa$`5M{7hEDfNqC-vaGEk}v;D z3-o{WHvd--G~55x+gV4&wQ~*sLa9N4LR+L1DU{-j05e0e;>F#)xDGH#ad-FPUW&VW zahKv+in}|MZ|L=VpY=ZPdjI%l5l)h`lkAi1!ne1!PiO%TyP@!+raqSV8}AJ4IuY@&;^tIblH|hNBUp??%WUhw|74i81i4X ziBJpgG4lXE=0A1xidiQ|z2hdp^N!{rZUCq3FgDpqDETvO!jf;ho3TPF*%*Vd9p#03 zFXa1*K`poq)k5V(zRtj@X7^LmEMem^_81<92SvpljKu+-Ez+ zhu`IkS=ZK4vANGSn5&vq+g8$zoMz_ouHA8Z$3y=%~LIw!rX8=Q?Kd(?Q_-FoYnuP^IcANe^ zewzj()5t423(_iKOiVvzewX`s|F?2#zuMZL)=&0xc1WWGWZ`mVhPS(oko`2czndWR z&l&XFUL~O0eHQ-HW+i{@BLD_Lm|=h0N5BzNRdzh~6tPp!)H2@rpxehEp+$hE|CsOU zlh!=~G;T6ibO_!(0?ntfVT5IR5k_&Jgn-#*@vp_=a}15c$I1xFJ{gpZ>P_!_nk`s| zT)(Rf(lf4D_sJKFD4Qf$xlTsx?rDiu%+6mnG|pc-ByO^ErCeNDmU1=+3HixTePL z(o`jF{Bk|>Lu~=u7N~)H;WJeE2(706 z&hX)*Ywb0v?E`1nhxCKbIM*FR3is=49sO>urfdg&IQVs&-%`);R!fmfyvVxmeK<)GBbMVrJ0MgX(O*opemA;G`7RvJj_`rzKL)I0S6;DE)G1 z5((G1?ZeE{UfO#xN^*`4iO}D*WPh5$tW0#xvhk*d&K_O$p{$Tb`d1Uza!H(T&%3%_ z3_4QVM-S6*U0kn83|+Fn$-}l*J49E+98ld<$@`JtC_44uuDC|Rq7p?pm^m{U`Xo!b|vrwlVO}%`Bb-j4Ro~ymR zMSuCpaYm9|i6jv}k@Q-QSy&_+6OSYl&eO4(N5DQvw%VzsTZ{{dtV0m7Z>9RI*54^mluu3*R!X8kP&iqh}Mig7V6|&|wJrAjy=7MpGWA$b1@CNoD2-^CX!D z)2c~x)(fFlNC>T_14Wez*(I5tVc=`?YXbFtUD`UumgkC0o7z4T*Cy`uo$8T+(iLSY z7eOz=8mVqnm2`o8%ns)ZR#wACqJ5 zB=v@>{?+_TQt1K$PEI?mgUSfFpkSvb{j@9W(N~4XUR*`Y6Ov6fMUO8j>AC1RXa>R< zO2pJ@YM14!)fz+^B!1|hYec$X&2Kh_X&s1TiT@x|#=j=mSNrmyHaavy*pTHQ z3xBfg$;n*{%pnFVqM$*lP(X5qwiT*#a_Z`u;1R|Jv7W0U#N>qcl2K=tc-&eX7FL;j6fdxZx2Q*iwuT3qREUUVkQ*|v z=SM=`1xYg%aMHNaQDX`@0C_`%7l&!qJ1lcdVlZ?I>r|X2f5ctoUAbRHlhcT2zN3+; zpmyrs?q*%S-z^bUi7D^VF$jmf|1efHsAx1}GNW}Bxc_i5x1H{FdUn@{ZcW>fJI!r%D9E*H_Bsy_Kw5?3- zf+W9X>cd?HBBuo5HD=2_M%rjj-by~~B9g7k_#zi!=4Km79FbPDSgh%j#kzM9mPPnB zZ#OlHZd~wdory0wwsmRc$CSDiV9O+J9*NmcE_b=&%tKyGM(*P zDyhCKM}O;TC#jPwEy%WSXgd9VHpPYc(d&)$=r!#Lms7qDgJ-jb1~C-`FNA4MCRQgZ z%}K7=*nFL8y`8(sb-BhDvy{v<*Hv94sadY-`|hEh6lDY81`XR`$k-cSf9<$wAVdTOdpR>$->5$DjyEA99!BP|;nhg7C7w6&&cI3;p?;l%D< zv|e$(#o+h2MzUO|-dH*5{iPJAqKcn$zlM!yZ|={H;Dyz1t9Gc~Ka%GkhBtf!&3f%c zc2}_JOXGJ_Bt2^EuBD)`%?ZdW zRmu&lNP?Jmi3Y5)V@qkg`lWhQ8S>H*;nrkb>`(T@hR|M3u*#0JKT~LDC>bRuCO4`? z7nw5IGsG-(G&aa^z?|4K)F*hh-t2fru^*L45pCV1a(3q1Y*w620e`$QZ?kS&3Q{N5 zE>oE!!#bdk6=ojPHg8?{n)Hz7J%+L@^8-?rmlbm-*8C5g zZ6L{W9#0rIc&IGDwL|g?rN_U3ET6GXQAkr|B?L*Wz*i#gfVxNa#EA}&USoM+F5DRC zF<>%0dShJ!U(0Zp_r&n122W)+XnIO^;9VBUbj_F&M^J;F|0iBi3bJoFZNcwGr$X)f zrl(-2H;+z?;ydP*JTmVQQDjG%9`IzVd1QE%JodV(Hxvf%mQh+L`Oi`xH0pnO>Vt+y z@JU{fK9NOvHYqgmS^+E0SA8X&kBLL{l%}K~RNLq*?-Fe%L;-#ze~?i#W=cWvu+(65 zB2Pdd$JFTf-KwuGxqV8{CuI|gtR+ab>8rK%K(QOXqoZc8unmW)1wJknpONU#??>8q zYAibS}GiPhNue&Dp6GvyxCGLRySuXQWRm! z%vv{8+#{Th0of*|@v-dh`77|r2`MWc*m4Fu;sft=L4$<14A+UOVjx2L?VTy2@8&;9 ze?oh5UmV;W$n?D5l`*UrojPe-hcxYx&vIXhZTEBf!WdKgtiWy*m4Xyy^#l&6LjmzK zm5jk>a-0rrx+#?w_pR~7HN1J!Q%NPBU{|eFgWoU(>#BB2Qny6C+-}}y?6b)F9z26d z++jZ4jT(H%q=jQpgF5!y!l(wNaV4%LJYv79<OEnUXsr!4f@mkEHO?Y6%0!0l>ad{ zvfP+5p#5MeKlW75eU3A0OFUp<4BmG>cJRK!!GKM#cfmZ=qid4RPd@s6PT=e59637S zszZSYoo+^@a-(v`w8iebF9G)PI?i^Y(E0N2ZhvG-`RXVp-m`L*Q?_v z8Bz%Tv&u=*&&q$s1f?4iw(PIQlyY#d87Z3n}O`&E__S6}*v z^_YUz*S&65ioGa$mh9oJ;jbsOzF}ZL752dhc_!@Dl)MGp_StnH}oo;c};WpgwF@yoNA4d~L`LTR5 zdz-1c78LD?INt|h4w?B-L`g)v8)gP6O|sh2iQJ`S;4tNXXyzKTwBM$`xEVT-m+dmU z*3nQ^{{1NbzI$@Q)%NTx|70x2iZg7sq3jf88Z~$E$G!4>gFe&5~EuW@7RU4i;a0IL9U`Z*Xw$m+oi!>HnBX91T-F5m41j z_z-&W1yGul==>p!w`e!D=fL+K|1f zhHi2+=j$rWGWS=_bYWip1{k6=a?Tj+9R)4UJ+}Jd>M24`mGJ_S=mq-1#@32NO{L4R z=)SS0wcP6tO&ZdoSbhgsEAl&e2j4J%@vP_0n->zKF{Hy6=o-ag#Rnw| zoBHT{>Ku%UJZD2We4qrrA=jkn0Qf#B@>|63ePW*Ain#j%n-?EIJ*w4eE>(&};T_=q zCXj&f!uK}noY&&JkTz~j0O}PU)@lVB$0Bo;G;X7tcZmfa;lrPpqgH;kc-!PR1L@#E z6~{gEL1Pze3VAs3#T3C#^G&viCRqP2yTqQs!72tTK3c--y_mZRH0|m*dp!M%qL0n- zRxKDG7s)l*q4qCd_2Z%do)w8Sd9go9#C#F_Vy4{EI`L2kEfKKKJ*VSUr(-43wi47009rrx&;AgAcdp~KW;HwWwu zl4iqpIS+OGBF-rc(ezNx2lXJ1$4WXLWPk0ft%@6G`uKgF zg!!2PzSY6IQ(xs#2LJsWR8my$4gieB$7(o=7}U zyoYZQb~59>v2I)hx?jM)u`F}oItGTN2G*gRS;h4WOVrxh8cxZ;IB+7*jh8V4nQS;ah zzaAGfT!{y!O}?gZ`q19bR@oLAVrYM5>MqdeX_ zN^$|1F;U))_SZINo9quCISGia+%%tm^bc1!>(^ezzih-?CUGOgqZ&;qvR=vF#%FVQ zx0GeJ=rB+S!`7QM)NIGlYxNx{(Mxq1i#y9*_EVbzw6|3i)+?|UPUXHYZnQsgg{5CB zoD-KCUoe{6FF%sV-=BE#rfrA#v9f*EMFSz>7u%Pn-aMfz32zvfb89^rp6eUzNs*cm4cGF@PxV?SFXe}AiyLHI? zb}DhxO4Y_S-JxCRv2!wq(sYM|=l$YCP!!DYLj+Qv+@gW3in)?v8P z0Ctdlw}n6DXuaN`aryMPp(77VjGruH9`4oloa8Z{`9&|&g-hIBrh%I0GaMGd0yzS3 zx}CJRyPxo2&uR^ss8t4YQ7h0tQhqZSS{G>zG<~Kojd=zMI24Z<3aml7wkaN$i=e%z zQ@H=*Ugvjhj*-_^AIS3g%^?_*@vixTn?vVkzQUT2^nnG;_t23e7R|^LW|`dNkhskJ zrd%14{)!xVgFvNsDfjZvgdsu7+ZQ} zV&9B$E@r>z_f6ef;AL5h1CVH;jMs^t$}KMLPo~M7y|B34un==t#shuE858XH|F zzj^?5ouU=@7YNM5O{(x+vpf1|0pH|@PtlovqVt6sYFk#d`=MUG5zkKRJZsB)#h=+Ohl%DpOGTHYM~6Qi+? z5}B_a z!DXZ}YVT4B2QH};dOLECR_6AMe_h;^y3d?9wTNMjeeN;CotoZ(e{yl3r$_m^*70RI zigMhY+B%QEhK2={T}gVz%>we=H)?(JOS>fK<|60??LTiF7rrqhq{mYT|Idxb8UJirS?l)IBsW;X+ zj%8L&+dQ%N&GB3+$-JhbT7wP0kb|b;?DA|D7KOMLCq+GqWZg@aHKM6yY#*HV0w?nR zq`qS_9L~9iSaGPA4P>A)W14}Najy-T`kY7(yuR@6VR6T%lC)&$MS2$$|(71ZgxHA9EC&UOMnDjt#=RuSCP9r=7{tKOGtJR79@_t*aOKsYKpcV?DXoz`sV$Oeb{E< z(5L8Dm-XzJ_NcQhwU0j(R+L@e@fG8Yf%RyV^UpIoE0WP~>~p7R#QPZ^WIJ3hI0q+% z$%%91Rvm*5TuIkIudkGNmzh3!?tLDJz)svKcpK6fQ^|=F`VII3{g~~%#C5G;0T}u+ zc;pa`1h08N6#EkRYHxBZ-AYWR?Uw>+6(ai#FC2%}m@U)2J+AweM!!E`Bnz%G1hU ztE%Ph3YONUmaCcO$yM{M#&tPv9LHOE0476mD|Y3IOX53n)2?x=#wkQBpN{v zG&X1j$pjofNX-wTRrnl`b6jSw70=!m#j;jIXS8XoGM#8-ZEAccMhP?3th{S>Mp?y= zqfE(kYG!6+;=9`gt*_~4)6Zt!cmuB`csIcYcg2rQxCKqKEyl+&kg76(0PewBV+@<8L4G30+)ntkxLHI!b%``t)81!RP~;K6kN7Gvgq^l$3!W zm#F;mAk3Md?so4nJ*&jMl4@(CwXrF@5E*Oec*UNpEELMaj!h-NjQ)~f{M%5*o@8#R z9vXdJV|5#osYq0Z8nzOHZk5MFUn=s#4Tnkl3EKG5N~Jtde`UzqLzgdOSsQ}*(fh%e#EomAhh6;r4>?@K7P{b zjHiX&^Xg>nlmua_ICeAc49QSiTx@Tkyy3o})eV#TQSa#2J*fViNl%GtX+Ee&B!7A5 z{D6NW(nU;gSz(Rcp9fEq-nCoG-8G%sdrj$l*&-yXmBnqWShP(0{4Qk{Ov|J>E!-tk zBbuaxQ~&PwE5#`uZac%gG@kqN`9rSznO>BuvkwnMj_yAdau(tYxugaevAZaB=~fax zc_E`3m&P*lZC%DMvNb5A7FJiPdF@H8gNc|9AxobQkG#1I-{Jm8ITGjR|+2gSRJ&y z(6_$wSj$Si4xHos<%*~5G@#M_eH2&z>1k)ndsRhvNLOlcn|y!+XpL4F|F={PMKt5GgX^wd8n7ug{m=%r<4o(+)r=I3h@Jog%*o}6;gz8C=;gEo+f z7&n)Hb+@yJv}&7le<$V0 zQ^9v#Ppd0=^@saI(Uz_cUMf{|^IKNWP}_ET`>joa zzScau@{uJ9+?0PI&K^x5#2$a9d2HIPqtlnA$V+cw{sU(fM$Z?K*nXRYFNfH zMRZ6MLbqID>0sx4Zjsq3uLI}?yeUvLm9J%)w^wDAz2Uo#y+9lb3p%AZyVTb`mw|Gw zP&49~1}9chb%60B^TcfmW0G9ih(Cv%=ptCo>a_WMW$1Ob-1uD7Tr}mk{+Ku*uX%Y z|9nXLXDWBoIk~Tx|onDe_TF0RQ|Bag(+HiRD=Jf z#X9oD5-=bco>|76z(a#rq&EwQ^b`e_cJN&jO3gEDSW2zdte7x2Ru5eT1+9d@nE@zd z%Zs7V(o4R?ZDFhUWUF|h*%j1E!r@_>_}*bhDI&W*XS$7hQfz96C4F$+_?t-6AKDe? zhm$L_lPlOdhZX@v?tR1TQd9?Vvw)4Dw0;Doa^HM$T%~efZoXb+EV0(0__H*1`VFr- zy+V#y)hN+@iPCZNf%nl9ib`(QJmf<~Yl^W%eM^dy_n3DBqlmc=HMDh;OE_ER-hbEB zh6gXJm({G6jw|JVxpWzx<|~>SfxswXg$1ig>>#fFC<`NM{es2I;<=5n5S*auHqv}YwV}KPjS?$Kw-W{zx zi%=q67EY*)-@>iTP#;&k({;xHYT&`4EzX(aPxnzXO1om&AljTRSqox>yOF02SeiEL7S%i`c;Hw7$k6%>RCIF@`f$X@+r>dw>DASv2@Kf z{SX~BjH9@R@@&puYS?Yvq<~s;deoP@0LWN~`jyswRNM{UW|oo6YeZx%V`?*uw@4a9oas7Rt#r16qxR zE-^o~et(sh_%274YP4Lo=$$a}Xt_L}Jln==(V>;j=Yndl3f4VL(*vE-i0gf=#?jKp zAg`zeXNyB`_(Nn`6^DpV8JD&hY-gtD`Zvh+YqJYPg6bwwxp2*9@IVu^U}{qdI{~|H zOvJdnf?E23x%DU8QmLoIQ@`lB8d-V%#b%EeU0e8XTl3-QAn}Xcn zP5ahk-$A)?)#i=3DS^e$Y4r!M^NC|{pA?b2!Pl!)$WU^J`nDBwvMo|zNYHs|(+v%g z2qFLV*h`%tyT)jRz?G+>>RWI@2vX@f>NuTz`ML#4)IqT?$ajAcxv;Mwc8%f1v+%gk z+lTmbOA8??VS7%WS$2TzAs3D`9?c=2YNFiscm3s-i>#79UF27s1)}JY+W<(_gaqZay+X*<*UWn zJz(W)KFP;CLnEx-#8IX@FE*mjlkbO0Wu;9>%BPg25Ui0(-8^27p?2YsTC*8R<~b+t zhIMp$S*B98Xtc3Zu$>h|yo?FNFQivuavPh@EoYc68~d74uDcWds7!mA4tJ)j>PFA% z`LV5(v-bf};M*1f}riNHdhU?AcbP4=0E&=&5xy+<8S_ma0t zC;cSonMEJmeT|0s5e?GngLw!!L2(nYxB}d-bN^3ACH-X7Zh25v7?}A_p7QsTa{kKj z{RzbViG%X~ii64;n(89Nb#0~Lrdno_GJk@hkiUYVED#tH33vMpvT4h#wV9*b_isK5JzJ~eSN!6C@9dX|!#wGkHxVRx zS9)V%4-ZbJ!!>uV2;#i`pu^FgL|p8Xz@{)i{4&>miK{(}b-z>NKnv0H%sMzu6>Z&A?ng~6zo}o0xKNBUOzaS~<&WyFvl4APfN>H* z%t}KUk-`<8a?RFJ7WLXu`*t1HT@EmWrTTRi2uep@&TvY@g5X|VIQ0azZqe=%6{K1T z{|9~(4EnRn{x|&Q|6l_Dk>o_O!oP!@NZ=BTggI}4%-_R*fG>ZZP4!p!^5>xbeVG4p zh?4+_40Ma2BEd@}|9Sf`e;57za4@84-}0wOEcB-cK++PfEu(9t0ze)^2awhMV5Rmq z!uTJZBB`aX3y{>Zyd{@GKN|r^>muORmfE@qGVrelNQvp{7;4=j(YHnjK~l?5RVT95}IL}rB~ zXE`|myhxpqjUpSlE&dk<`kw^%|6RlXn?AP)_Fwh+3lIC#jQ)S?^Ao=POaJ^FrQ4P6 z_ciTSAq4urh7BF?$G9P%=#PQB9Vih)9fS(m&(S8k?QUtZp9{|IEnc`a(h`{d+TLH5 zo}V4?bKw5~p+ybNi~&4wOC4RypZ0--zXmh$keLq%MAF(oDAF4M%7l!=eoaGokym86brN8P{(2vRTm^mx ze&iK-d^T@1Uub|(XMRq$M^0-NAzBgbXhy?@$F@spZOw1q# zATt#ZNOjxezq=!Ru+-H<2a~DEs7&G+WauDQ;_3v^(AdCe$@c$?axm}(9mWPys{!-= z(lg+n{esM6`e#|BQvlOH^#j9@?k4}N4+j3%cz|JG$iK?6{(IcOtf0S+ua%`1(sjl1 zcI}ZdwAV$N20$7PN4|V+CnnOfK*&rFj(ivXmf#~(A!8Td=I0k+V&dlKgTMq}EPT8W zC=A9dz>S4Y!NW1*+BDX6OA6%Q)Kv&xsVQoqVK>8a%VSKz`7Cu&fC^IiafL8zt mVSzC-3xN1}_@J=cDUGnwvb6eX!eCY=7zCY?l3zjq{r>^L?1i%c literal 0 HcmV?d00001 diff --git a/examples/tests_shared/resources/window_icon.1x.png b/examples/tests_shared/resources/window_icon.1x.png new file mode 100644 index 0000000000000000000000000000000000000000..9d28862a1559ca8804fed0b16b9d95107e0f8f7b GIT binary patch literal 603 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbL!WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8&jCImuK)l4s;#a4|NnnoUEQazKaB!={{R2?|No!=|9@Y& z^Ys7!zkmP#<4xJio4n`W|G(~Wt)IXCD&P9XA#G|@)oi}>J#!Afja&VC@y^?4&z=nm zuW?Q8{q*&F;gH3r@Q(8AaNNU*i@Bg2qb#Kq#d2;gh^Qs;1ayGu>PTpe~ zIc4td2ebA)bxr8tPTF(g@;$zky+(oE7w$f}aQEqjJ5Mw!PMo>**gSu;OKQJD@u~dH zK#gxt-+IiGyl2jy$1PhPTz~NN)7KyKcizq1_|`bEucf7B@2xj3>5~loJM9CCGd8@R zvH!V1%3jx`uFMVZvNyb~udfIClt;S%643jBB|(0{KrU4QL&lpG2Z4q!^mK6yskoJL z{;1F)2LacM?-Uzna;WUs4Fr?_|Cb69<(Rbj#nH9fGcWvWP(AYL;-O!5Qt5xTPo5QG z`XMgGGwa0GM_a7lw7+tkp}#5jSX57`+TJ_onja`1S!c{ws62;rPoUO2K`oXQGmkJH zdb62f7N7Ez{mq}dHvHlYZD9~(Io_R6u6RT=Px2uT*Yfs|&gZlDJltTZl+oFKwk3K? d&2NzehU3<`!7VF&;K literal 0 HcmV?d00001 diff --git a/examples/tests_shared/resources/window_icon.2x.png b/examples/tests_shared/resources/window_icon.2x.png new file mode 100644 index 0000000000000000000000000000000000000000..59b9d4928a0f9f8d491c4f972f010af7e75526da GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5XgaUj*T>t<7-_X!-;J^U}h6bP*kjo))<|~lJRubeF4CMa* z&#-y_lKnstH%}MGkcwML391Yd99juS_#-4FW`1DYz?5dgFr7(FO^ib+nvt7F;JJs* zycVs51DtN6Jz5IRj0;UXHC}HNU|=aWcW7W_;N@mLdP#km7SKcnPgg&ebxsLQ0KYXt A761SM literal 0 HcmV?d00001 diff --git a/examples/tests_shared/src/browser/client_app_browser.rs b/examples/tests_shared/src/browser/client_app_browser.rs new file mode 100644 index 0000000..e8f6a32 --- /dev/null +++ b/examples/tests_shared/src/browser/client_app_browser.rs @@ -0,0 +1,262 @@ +use crate::{ + browser::main_message_loop_external_pump::*, + common::{client_app::*, client_switches::*}, +}; +use cef::*; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +pub trait Delegate: Send { + fn on_before_command_line_processing( + &self, + _app: &Arc, + _command_line: &mut CommandLine, + ) { + } + + fn on_register_custom_preferences( + &self, + _app: &Arc, + _type_: PreferencesType, + _registrar: &mut PreferenceRegistrar, + ) { + } + + fn on_context_initialized(&self, _app: &Arc) {} + + fn on_already_running_app_relaunch( + &self, + _app: &Arc, + _command_line: &mut CommandLine, + _current_directory: &Path, + ) -> bool { + false + } + + fn default_client(&self, _app: &Arc) -> Option { + None + } +} + +pub struct ClientAppBrowser { + delegates: Vec>, +} + +impl ClientAppBrowser { + pub fn new(delegates: Vec>) -> Arc { + Arc::new(Self { delegates }) + } + + pub fn populate_settings( + command_line: Option, + cookieable_schemes: Vec, + settings: Settings, + ) -> Settings { + #[cfg(any(target_os = "windows", target_os = "linux"))] + let settings = { + Settings { + multi_threaded_message_loop: command_line.as_ref().map_or(0, |command_line| { + command_line.has_switch(Some(&CefString::from(MULTI_THREADED_MESSAGE_LOOP))) + }), + ..settings + } + }; + + let settings = if settings.multi_threaded_message_loop == 0 { + Settings { + external_message_pump: command_line.as_ref().map_or(0, |command_line| { + command_line.has_switch(Some(&CefString::from(EXTERNAL_MESSAGE_PUMP))) + }), + ..settings + } + } else { + settings + }; + + let cookieable_schemes_list = CefString::from(cookieable_schemes.join(",").as_str()); + + Settings { + cookieable_schemes_list, + ..settings + } + } + + pub fn delegates(&self) -> &[Box] { + &self.delegates + } +} + +wrap_app! { + pub struct ClientAppBrowserApp { + base: ClientApp, + client_app_browser: Arc, + } + + impl App { + fn on_before_command_line_processing( + &self, + process_type: Option<&CefString>, + command_line: Option<&mut CommandLine>, + ) { + let (Some(process_type), Some(command_line)) = (process_type, command_line) else { + return; + }; + + // Pass additional command-line flags to the browser process. + if process_type.to_string().is_empty() { + // Pass additional command-line flags when off-screen rendering is enabled. + if command_line.has_switch(Some(&CefString::from(OFF_SCREEN_RENDERING_ENABLED))) + != 0 + && command_line.has_switch(Some(&CefString::from(SHARED_TEXTURE_ENABLED))) == 0 + { + // Use software rendering and compositing (disable GPU) for increased FPS + // and decreased CPU usage. This will also disable WebGL so remove these + // switches if you need that capability. + // See https://github.com/chromiumembedded/cef/issues/1257 for details. + if command_line.has_switch(Some(&CefString::from(ENABLE_GPU))) == 0 { + command_line.append_switch(Some(&CefString::from("disable-gpu"))); + command_line + .append_switch(Some(&CefString::from("disable-gpu-compositing"))); + } + } + + if command_line.has_switch(Some(&CefString::from(USE_VIEWS))) != 0 + && command_line.has_switch(Some(&CefString::from("top-chrome-md"))) == 0 + { + // Use non-material mode on all platforms by default. Among other things + // this causes menu buttons to show hover state. See usage of + // MaterialDesignController::IsModeMaterial() in Chromium code. + command_line.append_switch_with_value( + Some(&CefString::from("top-chrome-md")), + Some(&CefString::from("non-material")), + ); + } + + // Disable the toolchain prompt on macOS. + #[cfg(target_os = "macos")] + command_line.append_switch(Some(&CefString::from("use-mock-keychain"))); + + // On Linux, in off screen rendering (OSR) shared texture mode, we must + // ensure that ANGLE uses the EGL backend. Without this, DMABUF based + // rendering will fail. The Chromium fallback path uses X11 pixmaps, + // which are only supported by Mesa drivers (e.g., AMD and Intel). + // + // While Mesa supports DMABUFs via both EGL and pixmaps, the EGL based + // DMA BUF import path is more robust and required for compatibility with + // drivers like NVIDIA that do not support pixmaps. + // + // We also append the kOzonePlatform switch with value x11 to ensure + // that X11 semantics are preserved, which is necessary for compatibility + // with some GDK/X11 integrations (e.g. Wayland with AMD). + #[cfg(target_os = "linux")] + if command_line.has_switch(Some(&CefString::from(OFF_SCREEN_RENDERING_ENABLED))) + != 0 + && command_line.has_switch(Some(&CefString::from(SHARED_TEXTURE_ENABLED))) != 0 + { + if command_line.has_switch(Some(&CefString::from(USE_ANGLE))) == 0 { + command_line.append_switch_with_value( + Some(&CefString::from(USE_ANGLE)), + Some(&CefString::from("gl-egl")), + ); + } + if command_line.has_switch(Some(&CefString::from(OZONE_PLATFORM))) == 0 { + command_line.append_switch_with_value( + Some(&CefString::from(OZONE_PLATFORM)), + Some(&CefString::from("X11")), + ); + } + } + } + + for delegate in self.client_app_browser.delegates() { + delegate.on_before_command_line_processing(&self.client_app_browser, command_line); + } + } + + fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { + self.base.on_register_custom_schemes(registrar); + } + + fn browser_process_handler(&self) -> Option { + Some(ClientAppBrowserProcessHandler::new( + self.client_app_browser.clone(), + )) + } + } +} + +wrap_browser_process_handler! { + pub struct ClientAppBrowserProcessHandler { + client_app_browser: Arc, + } + + impl BrowserProcessHandler { + fn on_register_custom_preferences( + &self, + type_: PreferencesType, + registrar: Option<&mut PreferenceRegistrar>, + ) { + let Some(registrar) = registrar else { + return; + }; + + for delegate in self.client_app_browser.delegates() { + delegate.on_register_custom_preferences(&self.client_app_browser, type_, registrar); + } + } + + fn on_context_initialized(&self) { + for delegate in self.client_app_browser.delegates() { + delegate.on_context_initialized(&self.client_app_browser); + } + } + + fn on_already_running_app_relaunch( + &self, + command_line: Option<&mut CommandLine>, + current_directory: Option<&CefString>, + ) -> i32 { + let (Some(command_line), Some(current_directory)) = (command_line, current_directory) + else { + return 0; + }; + + let delegates = self.client_app_browser.delegates(); + if !delegates.is_empty() { + let current_directory = PathBuf::from(current_directory.to_string().as_str()); + + for delegate in delegates { + if delegate.on_already_running_app_relaunch( + &self.client_app_browser, + command_line, + current_directory.as_path(), + ) { + return 1; + } + } + } + + 0 + } + + fn on_schedule_message_pump_work(&self, delay_ms: i64) { + if let Some(message_loop) = get_main_message_loop() { + if let Ok(mut message_loop) = message_loop.lock() { + message_loop.on_schedule_message_pump_work(delay_ms); + } + } + } + + fn default_client(&self) -> Option { + for delegate in self.client_app_browser.delegates() { + let client = delegate.default_client(&self.client_app_browser); + if client.is_some() { + return client; + } + } + None + } + } +} diff --git a/examples/tests_shared/src/browser/file_util.rs b/examples/tests_shared/src/browser/file_util.rs new file mode 100644 index 0000000..83506c9 --- /dev/null +++ b/examples/tests_shared/src/browser/file_util.rs @@ -0,0 +1,81 @@ +use cef::*; +use std::{ + fs::File, + io::{Read, Write}, + path::Path, +}; + +fn allow_file_io() -> bool { + currently_on(ThreadId::UI) == 0 && currently_on(ThreadId::IO) == 0 +} + +/// Reads the file at `path` into `contents` and returns true on success and +/// false on error. In case of I/O error, `contents` holds the data that could +/// be read from the file before the error occurred. When the file size exceeds +/// `max_size`, the function returns false with `contents` holding the file +/// truncated to `max_size`. `contents` may be [None], in which case this +/// function is useful for its side effect of priming the disk cache (could be +/// used for unit tests). Calling this function on the browser process UI or IO +/// threads is not allowed. +pub fn read_file_to_buffer>( + path: P, + contents: &mut Option>, + max_size: usize, +) -> bool { + if !allow_file_io() { + return false; + } + + if let Some(contents) = contents.as_mut() { + contents.clear(); + contents.reserve(max_size); + } + let Ok(mut file) = File::open(path) else { + return false; + }; + + // Many files supplied in `path` have incorrect size (proc files etc). + // Hence, the file is read sequentially as opposed to a one-shot read. + const BUFFER_SIZE: usize = 1 << 16; + let mut buffer = vec![0; BUFFER_SIZE]; + let mut size = 0; + + while let Ok(read) = file.read(&mut buffer) { + if read == 0 { + break; + } + + if let Some(contents) = contents.as_mut() { + contents.extend_from_slice(&buffer[..read.min(max_size - size)]); + } + + size += read; + if size > max_size { + return false; + } + } + + true +} + +/// Writes the given buffer into the file, overwriting any data that was +/// previously there. Returns the number of bytes written, or [None] on error. +/// Calling this function on the browser process UI or IO threads is not allowed. +pub fn write_file>(path: P, contents: &[u8]) -> Option { + if !allow_file_io() { + return None; + } + + let mut file = File::create(path).ok()?; + let mut size = 0; + + while size < contents.len() { + let write = file.write(&contents[size..]).unwrap_or(0); + if write == 0 { + break; + } + size += write; + } + + Some(size) +} diff --git a/examples/tests_shared/src/browser/geometry_util.rs b/examples/tests_shared/src/browser/geometry_util.rs new file mode 100644 index 0000000..8ed67e3 --- /dev/null +++ b/examples/tests_shared/src/browser/geometry_util.rs @@ -0,0 +1,56 @@ +use cef::*; + +pub const fn logical_value_to_device(value: i32, scale_factor: f32) -> i32 { + (value as f32 * scale_factor) as i32 +} + +pub const fn logical_rect_to_device(rect: Rect, scale_factor: f32) -> Rect { + Rect { + x: logical_value_to_device(rect.x, scale_factor), + y: logical_value_to_device(rect.y, scale_factor), + width: logical_value_to_device(rect.width, scale_factor), + height: logical_value_to_device(rect.height, scale_factor), + } +} + +pub const fn device_value_to_logical(value: i32, scale_factor: f32) -> i32 { + (value as f32 / scale_factor) as i32 +} + +pub const fn device_rect_to_logical(rect: Rect, scale_factor: f32) -> Rect { + Rect { + x: device_value_to_logical(rect.x, scale_factor), + y: device_value_to_logical(rect.y, scale_factor), + width: device_value_to_logical(rect.width, scale_factor), + height: device_value_to_logical(rect.height, scale_factor), + } +} + +pub const fn device_mouse_event_to_logical(event: MouseEvent, scale_factor: f32) -> MouseEvent { + MouseEvent { + x: device_value_to_logical(event.x, scale_factor), + y: device_value_to_logical(event.y, scale_factor), + ..event + } +} + +pub const fn device_touch_event_to_logical(event: TouchEvent, scale_factor: f32) -> TouchEvent { + TouchEvent { + x: event.x / scale_factor, + y: event.y / scale_factor, + ..event + } +} + +pub fn constrain_window_bounds(display: &Rect, window: &mut Rect) { + window.x = window.x.max(display.x); + window.y = window.y.max(display.y); + window.width = window.width.clamp(100, display.width); + window.height = window.height.clamp(100, display.height); + if window.x + window.width > display.x + display.width { + window.x = display.x + display.width - window.width; + } + if window.y + window.height > display.y + display.height { + window.y = display.y + display.height - window.height; + } +} diff --git a/examples/tests_shared/src/browser/main_message_loop.rs b/examples/tests_shared/src/browser/main_message_loop.rs new file mode 100644 index 0000000..b8d9c21 --- /dev/null +++ b/examples/tests_shared/src/browser/main_message_loop.rs @@ -0,0 +1,135 @@ +use cef::*; +use std::{ + mem, + sync::{Arc, Mutex, OnceLock}, +}; + +#[cfg(target_os = "windows")] +use windows_sys::Win32::Foundation::HWND; + +static INSTANCE: OnceLock>>>> = OnceLock::new(); + +pub fn get_main_message_loop() -> Arc>>> { + INSTANCE.get_or_init(|| Arc::new(Mutex::new(None))).clone() +} + +pub fn set_main_message_loop( + mut main_message_loop: Option>, +) -> Option> { + let instance = get_main_message_loop(); + let Ok(mut instance) = instance.lock() else { + return main_message_loop; + }; + mem::swap(&mut *instance, &mut main_message_loop); + main_message_loop +} + +pub fn currently_on_main_thread() -> bool { + let instance = get_main_message_loop(); + let Ok(instance) = instance.lock() else { + return false; + }; + let Some(instance) = instance.as_ref() else { + return false; + }; + instance.run_tasks_on_current_thread() +} + +pub fn main_post_task(task: Option<&mut Task>) { + let instance = get_main_message_loop(); + let Ok(mut instance) = instance.lock() else { + return; + }; + let Some(instance) = instance.as_mut() else { + return; + }; + instance.post_task(task); +} + +pub fn main_post_once(closure: Box) { + let instance = get_main_message_loop(); + let Ok(mut instance) = instance.lock() else { + return; + }; + let Some(instance) = instance.as_mut() else { + return; + }; + instance.post_once(closure); +} + +pub fn main_post_repeating(closure: Box) { + let instance = get_main_message_loop(); + let Ok(mut instance) = instance.lock() else { + return; + }; + let Some(instance) = instance.as_mut() else { + return; + }; + instance.post_repeating(closure); +} + +wrap_task! { + struct OnceClosure { + closure: Arc>>>, + } + + impl Task { + fn execute(&self) { + let Ok(mut closure) = self.closure.lock() else { + return; + }; + let Some(closure) = closure.take() else { + return; + }; + closure(); + } + } +} + +wrap_task! { + struct RepeatingClosure { + closure: Arc>>>, + } + + impl Task { + fn execute(&self) { + let Ok(mut closure) = self.closure.lock() else { + return; + }; + let Some(closure) = closure.as_mut() else { + return; + }; + closure(); + } + } +} + +pub trait MainMessageLoop: Send { + /// Run the message loop. The thread that this method is called on will be considered the main + /// thread. This blocks until [MainMessageLoop::quit] is called. + fn run(&mut self) -> i32; + + /// Quit the message loop. + fn quit(&mut self); + + /// Post a task for execution on the main message loop. + fn post_task(&mut self, task: Option<&mut Task>); + + /// Returns true if this message loop runs tasks on the current thread. + fn run_tasks_on_current_thread(&self) -> bool; + + #[cfg(target_os = "windows")] + fn set_current_modeless_dialog(&mut self, hwnd: HWND); + + /// Post a closure for execution on the main message loop. + fn post_once(&mut self, closure: Box) { + let mut task = OnceClosure::new(Arc::new(Mutex::new(Some(closure)))); + self.post_task(Some(&mut task)); + } + + /// Post a closure for execution on the main message loop. + fn post_repeating(&mut self, closure: Box) { + let mut task = RepeatingClosure::new(Arc::new(Mutex::new(Some(closure)))); + self.post_task(Some(&mut task)); + } +} diff --git a/examples/tests_shared/src/browser/main_message_loop_external_pump/linux.rs b/examples/tests_shared/src/browser/main_message_loop_external_pump/linux.rs new file mode 100644 index 0000000..2f4a425 --- /dev/null +++ b/examples/tests_shared/src/browser/main_message_loop_external_pump/linux.rs @@ -0,0 +1,243 @@ +use super::*; +use glib::*; +use std::{ + io::{self, Read, Write}, + os::fd::AsRawFd, + sync::{Arc, Mutex, Weak}, + thread, +}; + +/// Return a timeout suitable for the glib loop, -1 to block forever, +/// 0 to return right away, or a timeout in milliseconds from now. +fn get_time_interval_milliseconds(cef_time: &cef::Time) -> i32 { + let mut time = 0.0; + time_to_doublet(Some(cef_time), Some(&mut time)); + if time == 0.0 { + return -1; + } + + let mut cef_now = Default::default(); + time_now(Some(&mut cef_now)); + let mut now = 0.0; + time_to_doublet(Some(&cef_now), Some(&mut now)); + + // Be careful here. CefTime has a precision of microseconds, but we want a + // value in milliseconds. If there are 5.5ms left, should the delay be 5 or + // 6? It should be 6 to avoid executing delayed work too early. + let interval = (time - now).ceil() * 1000.0; + let interval = interval as i32; + + // If this value is negative, then we need to run delayed work soon. + interval.max(0) +} + +fn handle_eintr(mut callback: impl FnMut() -> io::Result) -> io::Result { + loop { + match callback() { + Err(err) if err.kind() == io::ErrorKind::Interrupted => continue, + result => break result, + } + } +} + +pub struct MainMessageLoopExternalPumpInner { + should_quit: bool, + main_context: MainContext, + work_source: Source, + timer_source: Option, + delayed_work_time: Arc>>, + wakeup_pipe_write: io::PipeWriter, +} + +impl Drop for MainMessageLoopExternalPumpInner { + fn drop(&mut self) { + self.work_source.destroy(); + } +} + +impl MainMessageLoopExternalPumpInner { + pub fn new(pump: &Weak>) -> Self { + let (mut wakeup_pipe_read, wakeup_pipe_write) = + io::pipe().expect("Failed to create wakeup pipe"); + let delayed_work_time = Arc::new(Mutex::new(None)); + let main_context = MainContext::default(); + let work_source = { + let pump = pump.clone(); + let delayed_work_time = delayed_work_time.clone(); + unix_fd_source_new( + wakeup_pipe_read.as_raw_fd(), + IOCondition::IN, + None, + Priority::DEFAULT_IDLE, + move |raw_fd, condition| { + let Some(pump) = pump.upgrade() else { + return ControlFlow::Break; + }; + let Ok(mut pump) = pump.lock() else { + return ControlFlow::Break; + }; + + // We usually have a single message on the wakeup pipe, since we are only + // signaled when the queue went from empty to non-empty, but there can be + // two messages if a task posted a task, hence we read at most two bytes. + // The glib poll will tell us whether there was data, so this read shouldn't + // block. + if condition.contains(IOCondition::IN) { + assert_eq!(wakeup_pipe_read.as_raw_fd(), raw_fd); + + let mut buffer = [0; 16]; + let size = handle_eintr(|| wakeup_pipe_read.read(&mut buffer)) + .expect("Error reading from the wakeup pipe."); + + match size { + 16 => { + let mut delay_ms = [0; 8]; + delay_ms.copy_from_slice(&buffer[..8]); + pump.on_schedule_work(i64::from_ne_bytes(delay_ms)); + delay_ms.copy_from_slice(&buffer[8..]); + pump.on_schedule_work(i64::from_ne_bytes(delay_ms)); + } + 8..16 => { + let mut delay_ms = [0; 8]; + delay_ms.copy_from_slice(&buffer[..8]); + pump.on_schedule_work(i64::from_ne_bytes(delay_ms)); + } + _ => {} + } + } + + if let Ok(delayed_work_time) = delayed_work_time.lock() { + let delay = delayed_work_time + .as_ref() + .map(get_time_interval_milliseconds) + .unwrap_or_default(); + + if delay == 0 { + // The timer has expired. That condition will stay true until we process + // that delayed work, so we don't need to record this differently. + pump.on_timer_timeout(); + } + } + + ControlFlow::Continue + }, + ) + }; + work_source.attach(Some(&main_context)); + + Self { + should_quit: false, + main_context, + work_source, + timer_source: None, + delayed_work_time, + wakeup_pipe_write, + } + } + + pub fn on_run(&mut self) -> bool { + // We really only do a single task for each iteration of the loop. If we + // have done something, assume there is likely something more to do. This + // will mean that we don't block on the message pump until there was nothing + // more to do. We also set this to true to make sure not to block on the + // first iteration of the loop. + let mut more_work_is_plausible = true; + + // We run our own loop instead of using g_main_loop_quit in one of the + // callbacks. This is so we only quit our own loops, and we don't quit + // nested loops run by others. + loop { + // Don't block if we think we have more work to do. + let block = !more_work_is_plausible; + + more_work_is_plausible = self.main_context.iteration(block); + if self.should_quit { + break; + } + } + + // We need to run the message pump until it is idle. However we don't have + // that information here so we run the message loop "for a while". + for _ in 0..10 { + // Do some work. + do_message_loop_work(); + + // Sleep to allow the CEF proc to do work. + thread::sleep(Duration::from_micros(50000)); + } + + false + } + + pub fn on_quit(&mut self) { + self.should_quit = true; + } + + pub fn on_schedule_message_pump_work(&mut self, delay: i64) { + let buffer = delay.to_ne_bytes(); + let size = handle_eintr(|| self.wakeup_pipe_write.write(&buffer)).unwrap_or_default(); + assert_eq!( + size, 8, + "Could not write to the UI message loop wakeup pipe!" + ); + } + + pub fn set_timer(&mut self, delay: i64) { + assert!(delay > 0); + + let mut delayed_work_time = self + .delayed_work_time + .lock() + .expect("Failed to lock delayed_work_time member"); + + let mut cef_now = Default::default(); + time_now(Some(&mut cef_now)); + let mut now = 0.0; + time_to_doublet(Some(&cef_now), Some(&mut now)); + + let time = now + delay as f64 / 1000.0; + let mut cef_time = Default::default(); + if time_from_doublet(time, Some(&mut cef_time)) == 0 { + panic!("Failed to convert time to CEF time"); + } + + *delayed_work_time = Some(cef_time); + + if let Some(timer_source) = self.timer_source.take() { + self.work_source.remove_child_source(&timer_source); + } + + let timer_source = timeout_source_new( + Duration::from_millis(delay.max(0).unsigned_abs()), + None, + Priority::DEFAULT_IDLE, + || ControlFlow::Continue, + ); + self.work_source.add_child_source(&timer_source); + } + + pub fn kill_timer(&mut self) { + let mut delayed_work_time = self + .delayed_work_time + .lock() + .expect("Failed to lock delayed_work_time member"); + + *delayed_work_time = None; + + if let Some(timer_source) = self.timer_source.take() { + self.work_source.remove_child_source(&timer_source); + } + } + + pub fn is_timer_pending(&self) -> bool { + let delayed_work_time = self + .delayed_work_time + .lock() + .expect("Failed to lock delayed_work_time member"); + let delay = delayed_work_time + .as_ref() + .map(get_time_interval_milliseconds) + .unwrap_or_default(); + delay > 0 + } +} diff --git a/examples/tests_shared/src/browser/main_message_loop_external_pump/mac.rs b/examples/tests_shared/src/browser/main_message_loop_external_pump/mac.rs new file mode 100644 index 0000000..eaee55d --- /dev/null +++ b/examples/tests_shared/src/browser/main_message_loop_external_pump/mac.rs @@ -0,0 +1,135 @@ +use super::*; +use objc2::{define_class, msg_send, rc::Retained, sel, AnyThread, DefinedClass}; +use objc2_app_kit::{NSApp, NSEventTrackingRunLoopMode}; +use objc2_foundation::{ + MainThreadMarker, NSNumber, NSObject, NSObjectNSThreadPerformAdditions, NSObjectProtocol, + NSRunLoop, NSRunLoopCommonModes, NSThread, NSTimer, +}; +use std::sync::{Mutex, Weak}; + +define_class! { + #[unsafe(super(NSObject))] + #[ivars = Weak>] + struct EventHandler; + + impl EventHandler { + #[unsafe(method(scheduleWork:))] + fn schedule_work(&self, delay_ms: &NSNumber) { + let Ok(delay_ms) = i64::try_from(delay_ms.integerValue()) else { + return; + }; + + let Some(pump) = self.ivars().upgrade() else { + return; + }; + let Ok(mut pump) = pump.lock() else { + return; + }; + + pump.on_schedule_work(delay_ms); + } + + #[unsafe(method(timerTimeout:))] + fn timer_timeout(&self, _: &NSTimer) { + let Some(pump) = self.ivars().upgrade() else { + return; + }; + let Ok(mut pump) = pump.lock() else { + return; + }; + + pump.on_timer_timeout(); + } + } + + unsafe impl NSObjectProtocol for EventHandler {} +} + +impl EventHandler { + fn new(pump: Weak>) -> Retained { + let this = Self::alloc().set_ivars(pump); + unsafe { msg_send![super(this), init] } + } +} + +pub struct MainMessageLoopExternalPumpInner { + owner_thread: Retained, + timer: Option>, + event_handler: Retained, +} + +unsafe impl Send for MainMessageLoopExternalPumpInner {} + +impl MainMessageLoopExternalPumpInner { + pub fn new(pump: &Weak>) -> Self { + let event_handler = EventHandler::new(pump.clone()); + Self { + owner_thread: NSThread::currentThread(), + timer: None, + event_handler, + } + } + + pub fn on_run(&mut self) -> bool { + let Some(mtm) = MainThreadMarker::new() else { + return false; + }; + NSApp(mtm).run(); + true + } + + pub fn on_quit(&mut self) { + let Some(mtm) = MainThreadMarker::new() else { + return; + }; + NSApp(mtm).stop(None); + } + + pub fn on_schedule_message_pump_work(&mut self, delay: i64) { + // This method may be called on any thread. + let delay = isize::try_from(delay).unwrap_or(isize::MAX); + let number = NSNumber::numberWithInteger(delay); + unsafe { + self.event_handler + .performSelector_onThread_withObject_waitUntilDone( + sel!(scheduleWork:), + &self.owner_thread, + Some(&number), + false, + ); + } + } + + pub fn set_timer(&mut self, delay: i64) { + let delay_s = delay as f64 / 1000.0; + let timer = unsafe { + NSTimer::timerWithTimeInterval_target_selector_userInfo_repeats( + delay_s, + &self.event_handler, + sel!(timerTimeout:), + None, + false, + ) + }; + + // Add the timer to default and tracking runloop modes. + let owner_runloop = NSRunLoop::currentRunLoop(); + unsafe { + owner_runloop.addTimer_forMode(&timer, NSRunLoopCommonModes); + owner_runloop.addTimer_forMode(&timer, NSEventTrackingRunLoopMode); + } + + self.timer = Some(timer); + } + + pub fn kill_timer(&mut self) { + let Some(timer) = self.timer.take() else { + return; + }; + timer.invalidate(); + } + + pub fn is_timer_pending(&self) -> bool { + self.timer.is_some() + } +} diff --git a/examples/tests_shared/src/browser/main_message_loop_external_pump/mod.rs b/examples/tests_shared/src/browser/main_message_loop_external_pump/mod.rs new file mode 100644 index 0000000..d1ba543 --- /dev/null +++ b/examples/tests_shared/src/browser/main_message_loop_external_pump/mod.rs @@ -0,0 +1,218 @@ +use super::{main_message_loop::*, main_message_loop_std::*}; +use cef::*; +use std::{ + sync::{Arc, Mutex, OnceLock, Weak}, + time::Duration, +}; +#[cfg(target_os = "windows")] +use windows_sys::Win32::Foundation::HWND; + +#[cfg(target_os = "windows")] +mod win; +#[cfg(target_os = "windows")] +use win::MainMessageLoopExternalPumpInner; + +#[cfg(target_os = "macos")] +mod mac; +#[cfg(target_os = "macos")] +use mac::MainMessageLoopExternalPumpInner; + +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "linux")] +use linux::MainMessageLoopExternalPumpInner; + +/// Special timer delay placeholder value. Intentionally 32-bit for Windows and +/// OS X platform API compatibility. +const TIMER_DELAY_PLACEHOLDER: i32 = i32::MAX; + +// The maximum number of milliseconds we're willing to wait between calls to +// [MainMessageLoopExternalPump::do_work]. +const MAX_TIMER_DELAY: i64 = 1000 / 30; // 30fps + +static INSTANCE: OnceLock>> = OnceLock::new(); + +pub fn get_main_message_loop() -> Option>> { + INSTANCE.get()?.upgrade() +} + +pub fn set_main_message_loop( + main_message_loop: Option>>, +) -> Option>> { + let main_message_loop = main_message_loop + .as_ref() + .map(Arc::downgrade) + .unwrap_or_default(); + if let Err(instance) = INSTANCE.set(main_message_loop) { + instance.upgrade() + } else { + None + } +} + +pub struct MainMessageLoopExternalPump { + standard_message_loop: Arc>>>, + is_active: bool, + reentrancy_detected: bool, + inner: MainMessageLoopExternalPumpInner, +} + +impl MainMessageLoopExternalPump { + pub fn new() -> Arc> { + let external_pump = Arc::new_cyclic(|weak_ref| { + Mutex::new(Self { + standard_message_loop: MainMessageLoopStd::new(), + is_active: false, + reentrancy_detected: false, + inner: MainMessageLoopExternalPumpInner::new(weak_ref), + }) + }); + set_main_message_loop(Some(external_pump.clone())); + external_pump + } + + /// Called from [BrowserProcessHandler::on_schedule_message_pump_work] on any thread. + /// The platform subclass must implement this method and schedule a call to + /// [MainMessageLoopExternalPump::on_schedule_work] on the main application thread. + pub fn on_schedule_message_pump_work(&mut self, delay: i64) { + self.inner.on_schedule_message_pump_work(delay); + } + + fn on_schedule_work(&mut self, delay: i64) { + assert!(currently_on_main_thread()); + + if delay == i64::from(TIMER_DELAY_PLACEHOLDER) && self.is_timer_pending() { + // Don't set the maximum timer requested from DoWork() if a timer event is + // currently pending. + return; + } + + self.kill_timer(); + + let delay = if delay <= 0 { + // Execute the work immediately. + self.do_work(); + 0 + } else { + // Never wait longer than the maximum allowed time. + delay.min(MAX_TIMER_DELAY) + }; + + // Results in call to on_timer_timeout after the specified delay. + self.set_timer(delay); + } + + fn on_timer_timeout(&mut self) { + assert!(currently_on_main_thread()); + self.kill_timer(); + self.do_work(); + } + + /// Control the pending work timer in the platform subclass. Only called on + /// the main application thread. + fn set_timer(&mut self, delay: i64) { + assert!(!self.is_timer_pending()); + assert!(delay > 0); + self.inner.set_timer(delay); + } + + /// Control the pending work timer in the platform subclass. Only called on + /// the main application thread. + fn kill_timer(&mut self) { + self.inner.kill_timer(); + } + + /// Control the pending work timer in the platform subclass. Only called on + /// the main application thread. + fn is_timer_pending(&self) -> bool { + self.inner.is_timer_pending() + } + + /// Handle work processing. + fn do_work(&mut self) { + let was_reentrant = self.perform_message_loop_work(); + if was_reentrant { + self.on_schedule_message_pump_work(0); + } else if !self.is_timer_pending() { + self.on_schedule_message_pump_work(i64::from(TIMER_DELAY_PLACEHOLDER)); + } + } + + fn perform_message_loop_work(&mut self) -> bool { + if self.is_active { + // When do_message_loop_work is called there may be various callbacks + // (such as paint and IPC messages) that result in additional calls to this + // method. If re-entrancy is detected we must repost a request again to the + // owner thread to ensure that the discarded call is executed in the future. + self.reentrancy_detected = true; + return false; + } + + self.reentrancy_detected = false; + + self.is_active = true; + do_message_loop_work(); + self.is_active = false; + + // `reentrancy_detected` may have changed due to re-entrant calls to this + // method. + self.reentrancy_detected + } +} + +impl MainMessageLoop for MainMessageLoopExternalPump { + fn run(&mut self) -> i32 { + if !self.inner.on_run() { + return 0; + } + + self.kill_timer(); + + // We need to run the message pump until it is idle. However we don't have + // that information here so we run the message loop "for a while". + for _ in 0..10 { + // Do some work. + do_message_loop_work(); + + // Sleep to allow the CEF proc to do work. + std::thread::sleep(Duration::from_millis(50)); + } + + 0 + } + + fn quit(&mut self) { + self.inner.on_quit(); + } + + fn post_task(&mut self, task: Option<&mut Task>) { + let Ok(mut standard_message_loop) = self.standard_message_loop.lock() else { + return; + }; + let Some(standard_message_loop) = standard_message_loop.as_mut() else { + return; + }; + standard_message_loop.post_task(task); + } + + fn run_tasks_on_current_thread(&self) -> bool { + let Ok(standard_message_loop) = self.standard_message_loop.lock() else { + return false; + }; + let Some(standard_message_loop) = standard_message_loop.as_ref() else { + return false; + }; + standard_message_loop.run_tasks_on_current_thread() + } + + #[cfg(target_os = "windows")] + fn set_current_modeless_dialog(&mut self, hwnd: HWND) { + let Ok(mut standard_message_loop) = self.standard_message_loop.lock() else { + return; + }; + let Some(standard_message_loop) = standard_message_loop.as_mut() else { + return; + }; + standard_message_loop.set_current_modeless_dialog(hwnd); + } +} diff --git a/examples/tests_shared/src/browser/main_message_loop_external_pump/win.rs b/examples/tests_shared/src/browser/main_message_loop_external_pump/win.rs new file mode 100644 index 0000000..61be6d7 --- /dev/null +++ b/examples/tests_shared/src/browser/main_message_loop_external_pump/win.rs @@ -0,0 +1,145 @@ +use super::*; +use crate::browser::util_win::*; +use std::{ + mem, ptr, + sync::{Mutex, Weak}, +}; +use windows_sys::{w, Win32::Foundation::*, Win32::UI::WindowsAndMessaging::*}; + +const MSG_HAVE_WORK: u32 = WM_USER + 1; + +pub struct MainMessageLoopExternalPumpInner { + timer_pending: bool, + main_thread_target: Option, +} + +impl MainMessageLoopExternalPumpInner { + pub fn new(pump: &Weak>) -> Self { + let main_thread_target = { + const WINDOW_CLASS_NAME: *const u16 = w!("MainMessageLoopExternalPump"); + + let instance = get_code_module_handle(); + unsafe { + let wcex = WNDCLASSEXW { + cbSize: mem::size_of::() as u32, + lpfnWndProc: Some(Self::window_proc), + hInstance: instance, + lpszClassName: WINDOW_CLASS_NAME, + ..mem::zeroed() + }; + RegisterClassExW(&wcex); + + // Create the message handling window. + let main_thread_target = CreateWindowExW( + 0, + WINDOW_CLASS_NAME, + ptr::null(), + WS_OVERLAPPEDWINDOW, + 0, + 0, + 0, + 0, + HWND_MESSAGE, + ptr::null_mut(), + instance, + ptr::null(), + ); + assert!(!main_thread_target.is_null()); + main_thread_target + } + }; + + set_user_data(main_thread_target, Some(pump.clone())); + + Self { + timer_pending: false, + main_thread_target: Some(main_thread_target as usize), + } + } + + unsafe extern "system" fn window_proc( + hwnd: HWND, + message: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + match message { + MSG_HAVE_WORK | WM_TIMER => { + if let Some(message_loop) = + get_user_data::>>(hwnd) + { + if let Some(message_loop) = message_loop.upgrade() { + if let Ok(mut message_loop) = message_loop.lock() { + if message == MSG_HAVE_WORK { + let delay = lparam as i64; + message_loop.on_schedule_work(delay); + } else { + message_loop.on_timer_timeout(); + } + } + } + } + } + WM_DESTROY => { + let _ = set_user_data::>>(hwnd, None); + } + _ => {} + } + DefWindowProcW(hwnd, message, wparam, lparam) + } + + pub fn on_run(&mut self) -> bool { + let mut msg = Default::default(); + unsafe { + while GetMessageW(&mut msg, ptr::null_mut(), 0, 0) != 0 { + TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + true + } + + pub fn on_quit(&mut self) { + unsafe { + PostMessageW(ptr::null_mut(), WM_QUIT, 0, 0); + } + } + + pub fn on_schedule_message_pump_work(&mut self, delay: i64) { + // This method may be called on any thread. + unsafe { + if let Some(main_thread_target) = self.main_thread_target { + PostMessageW( + main_thread_target as HWND, + MSG_HAVE_WORK, + 0, + delay as LPARAM, + ); + } + } + } + + pub fn set_timer(&mut self, delay: i64) { + self.timer_pending = true; + if let Some(main_thread_target) = self.main_thread_target { + unsafe { + SetTimer(main_thread_target as HWND, 1, delay as u32, None); + } + } + } + + pub fn kill_timer(&mut self) { + if self.timer_pending { + if let Some(main_thread_target) = self.main_thread_target { + unsafe { + KillTimer(main_thread_target as HWND, 1); + } + } + self.timer_pending = false; + } + } + + pub fn is_timer_pending(&self) -> bool { + self.timer_pending + } +} diff --git a/examples/tests_shared/src/browser/main_message_loop_std.rs b/examples/tests_shared/src/browser/main_message_loop_std.rs new file mode 100644 index 0000000..d6ccf75 --- /dev/null +++ b/examples/tests_shared/src/browser/main_message_loop_std.rs @@ -0,0 +1,46 @@ +use super::main_message_loop::*; +use cef::*; +use std::sync::{Arc, Mutex}; + +#[cfg(target_os = "windows")] +use windows_sys::Win32::Foundation::HWND; + +pub struct MainMessageLoopStd; + +impl MainMessageLoopStd { + pub fn new() -> Arc>>> { + set_main_message_loop(Some(Box::new(MainMessageLoopStd))); + get_main_message_loop() + } +} + +impl Drop for MainMessageLoopStd { + fn drop(&mut self) { + set_main_message_loop(None); + } +} + +impl MainMessageLoop for MainMessageLoopStd { + fn run(&mut self) -> i32 { + run_message_loop(); + 0 + } + + fn quit(&mut self) { + quit_message_loop(); + } + + fn post_task(&mut self, task: Option<&mut Task>) { + post_task(ThreadId::UI, task); + } + + fn run_tasks_on_current_thread(&self) -> bool { + currently_on(ThreadId::UI) != 0 + } + + #[cfg(target_os = "windows")] + fn set_current_modeless_dialog(&mut self, _hwnd: HWND) { + // Nothing to do here. The Chromium message loop implementation will internally route + // dialog messages. + } +} diff --git a/examples/tests_shared/src/browser/mod.rs b/examples/tests_shared/src/browser/mod.rs new file mode 100644 index 0000000..7c724a2 --- /dev/null +++ b/examples/tests_shared/src/browser/mod.rs @@ -0,0 +1,10 @@ +pub mod client_app_browser; +pub mod file_util; +pub mod geometry_util; +pub mod main_message_loop; +pub mod main_message_loop_external_pump; +pub mod main_message_loop_std; +pub mod resource_util; + +#[cfg(target_os = "windows")] +pub mod util_win; diff --git a/examples/tests_shared/src/browser/resource_util/mod.rs b/examples/tests_shared/src/browser/resource_util/mod.rs new file mode 100644 index 0000000..84b172a --- /dev/null +++ b/examples/tests_shared/src/browser/resource_util/mod.rs @@ -0,0 +1,9 @@ +#[cfg(target_os = "windows")] +pub mod win; +#[cfg(target_os = "windows")] +pub use win::*; + +#[cfg(not(target_os = "windows"))] +pub mod posix; +#[cfg(not(target_os = "windows"))] +pub use posix::*; diff --git a/examples/tests_shared/src/browser/resource_util/posix.rs b/examples/tests_shared/src/browser/resource_util/posix.rs new file mode 100644 index 0000000..4f31a97 --- /dev/null +++ b/examples/tests_shared/src/browser/resource_util/posix.rs @@ -0,0 +1,42 @@ +use cef::*; +use std::{fs::File, io::Read, path::PathBuf}; + +pub fn get_resource_directory() -> Option { + let mut path = std::env::current_exe().ok()?; + + // Pop the executable file name. + path.pop(); + + #[cfg(target_os = "macos")] + { + // Pop the MacOS directory. + path.pop(); + path.push("Resources"); + } + + #[cfg(target_os = "linux")] + { + path.push("files"); + } + + Some(path) +} + +pub fn load_binary_resource(resource_name: &str) -> Option> { + let path = get_resource_directory()?.join(resource_name); + let mut file = File::open(path).ok()?; + let mut data = Vec::new(); + file.read_to_end(&mut data).ok()?; + Some(data) +} + +pub fn get_binary_resource_reader(resource_name: &str) -> Option { + let path = get_resource_directory()?.join(resource_name); + if !path.exists() { + return None; + } + + let path = path.to_str()?; + let path = CefString::from(path); + stream_reader_create_for_file(Some(&CefString::from(path))) +} diff --git a/examples/tests_shared/src/browser/resource_util/win.rs b/examples/tests_shared/src/browser/resource_util/win.rs new file mode 100644 index 0000000..42493af --- /dev/null +++ b/examples/tests_shared/src/browser/resource_util/win.rs @@ -0,0 +1,124 @@ +use crate::browser::util_win::*; +use cef::{ + wrapper::{byte_read_handler::*, resource_manager::*, stream_resource_handler::*}, + *, +}; +use std::{ + mem, + sync::{Arc, Mutex, OnceLock}, +}; +use windows_sys::Win32::System::LibraryLoader::{ + FindResourceW, LoadResource, LockResource, SizeofResource, +}; + +pub type GetResourceId = Box u16>; + +static INSTANCE: OnceLock>>> = OnceLock::new(); + +pub fn get_fn_get_resource_id() -> Arc>> { + INSTANCE.get_or_init(|| Arc::new(Mutex::new(None))).clone() +} + +pub fn set_fn_get_resource_id(mut get_resource_id: Option) -> Option { + let instance = get_fn_get_resource_id(); + let Ok(mut instance) = instance.lock() else { + return get_resource_id; + }; + mem::swap(&mut *instance, &mut get_resource_id); + get_resource_id +} + +pub fn load_binary_resource(resource_name: &str) -> Option> { + let get_resource_id = get_fn_get_resource_id(); + let get_resource_id = get_resource_id.lock().ok()?; + let get_resource_id = get_resource_id.as_ref()?; + + let resource_id = (*get_resource_id)(resource_name); + let instance = get_code_module_handle(); + + unsafe { + // Defined in https://github.com/chromiumembedded/cef/blob/master/tests/cefclient/browser/resource.h + const RT_BINARY: u16 = 256; + + let resource = FindResourceW( + instance, + resource_id as usize as *const _, + RT_BINARY as usize as *const _, + ); + if resource.is_null() { + return None; + } + + let data = LoadResource(instance, resource); + if data.is_null() { + return None; + } + + let size = SizeofResource(instance, resource); + if size == 0 { + return None; + } + + let ptr = LockResource(data); + if ptr.is_null() { + return None; + } + + Some(std::slice::from_raw_parts(ptr as *const u8, size as usize).to_vec()) + } +} + +pub fn get_binary_resource_reader(resource_name: &str) -> Option { + let data = load_binary_resource(resource_name)?; + let stream = ByteStream::new(data); + let mut handler = ByteReadHandler::new(Arc::new(Mutex::new(stream))); + stream_reader_create_for_handler(Some(&mut handler)) +} + +struct BinaryResourceProvider { + url_path: String, + resource_path_prefix: String, +} + +impl BinaryResourceProvider { + fn new(url_path: &str, resource_path_prefix: &str) -> Self { + Self { + url_path: normalize_url_path(url_path), + resource_path_prefix: normalize_url_path(resource_path_prefix), + } + } +} + +impl ResourceManagerProvider for BinaryResourceProvider { + fn on_request(&self, request: Arc>) -> bool { + assert_ne!( + currently_on(ThreadId::IO), + 0, + "on_request must be called on the IO thread" + ); + + let Ok(mut request) = request.lock() else { + return false; + }; + let url = request.url(); + let Some(relative_path) = url.strip_prefix(self.url_path.as_str()) else { + // Not handled by this provider. + return false; + }; + + let mime_type = request.mime_type_resolver()(url); + let relative_path = format!("{}/{relative_path}", self.resource_path_prefix); + let handler = get_binary_resource_reader(&relative_path) + .map(|stream| StreamResourceHandler::new_with_stream(mime_type, stream)); + + request.continue_request(handler); + true + } +} + +pub fn create_binary_resource_provider( + url_path: &str, + resource_path_prefix: &str, +) -> Box { + Box::new(BinaryResourceProvider::new(url_path, resource_path_prefix)) +} diff --git a/examples/tests_shared/src/browser/util_win.rs b/examples/tests_shared/src/browser/util_win.rs new file mode 100644 index 0000000..5491b87 --- /dev/null +++ b/examples/tests_shared/src/browser/util_win.rs @@ -0,0 +1,256 @@ +use std::{ffi::c_void, mem, ptr, sync::OnceLock}; +use windows_sys::Win32::{ + Foundation::*, + Graphics::Gdi::*, + System::{ + LibraryLoader::{ + GetModuleHandleExW, GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + }, + Performance::*, + SystemServices::*, + }, + UI::{Input::KeyboardAndMouse::*, WindowsAndMessaging::*}, +}; + +pub fn get_time_now() -> u64 { + static FREQUENCY: OnceLock = OnceLock::new(); + + let frequency = FREQUENCY.get_or_init(|| { + let mut frequency = 0; + unsafe { QueryPerformanceFrequency(&mut frequency) }; + frequency.max(1) as f64 / 1000000.0 + }); + + let mut current_time = 0; + unsafe { QueryPerformanceCounter(&mut current_time) }; + ((current_time as f64 / *frequency) as i64).max(0) as u64 +} + +pub fn set_user_data_ptr(hwnd: HWND, data: *mut c_void) -> *mut c_void { + unsafe { + SetLastError(ERROR_SUCCESS); + let result = SetWindowLongPtrW(hwnd, GWLP_USERDATA, data as isize); + assert!(result != 0 || GetLastError() == ERROR_SUCCESS); + result as *mut _ + } +} + +pub fn set_user_data(hwnd: HWND, data: Option) -> Option> { + let ptr: *mut T = set_user_data_ptr( + hwnd, + data.map(|data| Box::into_raw(Box::new(data)).cast()) + .unwrap_or(ptr::null_mut()), + ) + .cast(); + if ptr.is_null() { + None + } else { + unsafe { Some(Box::from_raw(ptr)) } + } +} + +pub fn get_user_data<'a, T>(hwnd: HWND) -> Option<&'a mut T> { + unsafe { + let ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut T; + ptr.as_mut() + } +} + +pub fn set_window_proc_ptr(hwnd: HWND, proc: WNDPROC) -> WNDPROC { + Some(unsafe { + let old = GetWindowLongPtrW(hwnd, GWLP_WNDPROC); + assert_ne!(old, 0); + if let Some(proc) = proc { + let result = SetWindowLongPtrW(hwnd, GWLP_WNDPROC, mem::transmute(proc)); + assert!(result != 0 || GetLastError() == ERROR_SUCCESS); + } + mem::transmute(old) + }) +} + +#[repr(i32)] +#[derive(Default, Debug)] +pub enum EventFlag { + #[default] + None = 0, + CapsLockOn = 1 << 0, + ShiftDown = 1 << 1, + ControlDown = 1 << 2, + AltDown = 1 << 3, + LeftMouseButton = 1 << 4, + MiddleMouseButton = 1 << 5, + RightMouseButton = 1 << 6, + CommandDown = 1 << 7, + NumLockOn = 1 << 8, + IsKeyPad = 1 << 9, + IsLeft = 1 << 10, + IsRight = 1 << 11, + AltGrDown = 1 << 12, + IsRepeat = 1 << 13, + PrecisionScrollingDelta = 1 << 14, + ScrollByPage = 1 << 15, +} + +pub fn get_resource_string(id: u32) -> String { + let mut buffer = [0u16; 100]; + String::from_utf16_lossy(unsafe { + let len = LoadStringW( + get_code_module_handle(), + id, + buffer.as_mut_ptr(), + buffer.len() as i32, + ) as usize; + &buffer[..len] + }) +} + +pub fn get_cef_mouse_modifiers(wparam: WPARAM) -> i32 { + let mut modifiers = 0; + + if wparam & MK_CONTROL as usize != 0 { + modifiers |= EventFlag::ControlDown as i32; + } + if wparam & MK_SHIFT as usize != 0 { + modifiers |= EventFlag::ShiftDown as i32; + } + if is_key_down(VK_MENU as _) { + modifiers |= EventFlag::AltDown as i32; + } + if wparam & MK_LBUTTON as usize != 0 { + modifiers |= EventFlag::LeftMouseButton as i32; + } + if wparam & MK_MBUTTON as usize != 0 { + modifiers |= EventFlag::MiddleMouseButton as i32; + } + if wparam & MK_RBUTTON as usize != 0 { + modifiers |= EventFlag::RightMouseButton as i32; + } + + // Low bit set from GetKeyState indicates "toggled". + unsafe { + if GetKeyState(VK_NUMLOCK as i32) & 1 != 0 { + modifiers |= EventFlag::NumLockOn as i32; + } + if GetKeyState(VK_CAPITAL as i32) & 1 != 0 { + modifiers |= EventFlag::CapsLockOn as i32; + } + } + + modifiers +} + +pub fn get_cef_keyboard_modifiers(wparam: WPARAM, lparam: LPARAM) -> i32 { + let mut modifiers = 0; + + if is_key_down(VK_SHIFT as _) { + modifiers |= EventFlag::ShiftDown as i32; + } + if is_key_down(VK_CONTROL as _) { + modifiers |= EventFlag::ControlDown as i32; + } + if is_key_down(VK_MENU as _) { + modifiers |= EventFlag::AltDown as i32; + } + + // Low bit set from GetKeyState indicates "toggled". + unsafe { + if GetKeyState(VK_NUMLOCK as i32) & 1 != 0 { + modifiers |= EventFlag::NumLockOn as i32; + } + if GetKeyState(VK_CAPITAL as i32) & 1 != 0 { + modifiers |= EventFlag::CapsLockOn as i32; + } + } + + match wparam as VIRTUAL_KEY { + VK_RETURN => { + if (lparam >> 16) & KF_EXTENDED as isize != 0 { + modifiers |= EventFlag::IsKeyPad as i32; + } + } + VK_INSERT | VK_DELETE | VK_HOME | VK_END | VK_PRIOR | VK_NEXT | VK_UP | VK_DOWN + | VK_LEFT | VK_RIGHT => { + if (lparam >> 16) & KF_EXTENDED as isize == 0 { + modifiers |= EventFlag::IsKeyPad as i32; + } + } + VK_NUMLOCK | VK_NUMPAD0 | VK_NUMPAD1 | VK_NUMPAD2 | VK_NUMPAD3 | VK_NUMPAD4 + | VK_NUMPAD5 | VK_NUMPAD6 | VK_NUMPAD7 | VK_NUMPAD8 | VK_NUMPAD9 | VK_DIVIDE + | VK_MULTIPLY | VK_SUBTRACT | VK_ADD | VK_DECIMAL | VK_CLEAR => { + modifiers |= EventFlag::IsKeyPad as i32; + } + VK_SHIFT => { + if is_key_down(VK_LSHIFT as _) { + modifiers |= EventFlag::IsLeft as i32; + } else if is_key_down(VK_RSHIFT as _) { + modifiers |= EventFlag::IsRight as i32; + } + } + VK_CONTROL => { + if is_key_down(VK_LCONTROL as _) { + modifiers |= EventFlag::IsLeft as i32; + } else if is_key_down(VK_RCONTROL as _) { + modifiers |= EventFlag::IsRight as i32; + } + } + VK_MENU => { + if is_key_down(VK_LMENU as _) { + modifiers |= EventFlag::IsLeft as i32; + } else if is_key_down(VK_RMENU as _) { + modifiers |= EventFlag::IsRight as i32; + } + } + VK_LWIN => { + modifiers |= EventFlag::IsLeft as i32; + } + VK_RWIN => { + modifiers |= EventFlag::IsRight as i32; + } + _ => {} + } + + modifiers +} + +pub fn is_key_down(wparam: WPARAM) -> bool { + unsafe { + let key_state = GetKeyState(wparam as i32) as u16; + key_state & 0x8000 != 0 + } +} + +pub fn get_device_scale_factor() -> f32 { + static SCALE_FACTOR: OnceLock = OnceLock::new(); + + *SCALE_FACTOR.get_or_init(|| unsafe { + let screen_dc = GetDC(ptr::null_mut()); + let dpi_x = GetDeviceCaps(screen_dc, LOGPIXELSX as i32); + ReleaseDC(ptr::null_mut(), screen_dc); + dpi_x as f32 / 96.0 + }) +} + +pub fn get_code_module_handle() -> HINSTANCE { + let mut module: HMODULE = ptr::null_mut(); + unsafe { + let result = GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + get_code_module_handle as *const _, + &mut module, + ); + assert_ne!(result, 0); + } + module +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_time_now() { + let time = get_time_now(); + assert!(time > 0); + } +} diff --git a/examples/tests_shared/src/common/binary_value_utils.rs b/examples/tests_shared/src/common/binary_value_utils.rs new file mode 100644 index 0000000..0ca92b4 --- /dev/null +++ b/examples/tests_shared/src/common/binary_value_utils.rs @@ -0,0 +1,147 @@ +use cef::*; +use std::{ + fmt::Debug, + sync::OnceLock, + time::{Duration, Instant}, +}; + +pub const TEST_SEND_PROCESS_MESSAGE: &[u8] = b"testSendProcessMessage"; +pub const TEST_SEND_SMR_PROCESS_MESSAGE: &[u8] = b"testSendSMRProcessMessage"; + +#[derive(Debug)] +pub struct MessageId { + pub id: u32, +} + +impl From for MessageId { + fn from(id: u32) -> Self { + Self { id } + } +} + +impl From<&[u8]> for MessageId { + fn from(bytes: &[u8]) -> Self { + assert_eq!(bytes.len(), 4); + let mut id = [0; 4]; + id.copy_from_slice(bytes); + Self::from(u32::from_ne_bytes(id)) + } +} + +impl From<&MessageId> for [u8; 4] { + fn from(id: &MessageId) -> Self { + id.id.to_ne_bytes() + } +} + +pub struct ElapsedMicros { + duration: u128, +} + +impl ElapsedMicros { + pub fn now() -> Self { + static START_TIME: OnceLock = OnceLock::new(); + let start_time = START_TIME.get_or_init(Instant::now); + + Self { + duration: start_time.elapsed().as_micros(), + } + } +} + +impl From<&[u8]> for ElapsedMicros { + fn from(bytes: &[u8]) -> Self { + assert_eq!(bytes.len(), 16); + let mut duration = [0; 16]; + duration.copy_from_slice(bytes); + Self { + duration: u128::from_ne_bytes(duration), + } + } +} + +impl From<&ElapsedMicros> for [u8; 16] { + fn from(elapsed: &ElapsedMicros) -> Self { + elapsed.duration.to_ne_bytes() + } +} + +impl Debug for ElapsedMicros { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let duration = u64::try_from(self.duration).unwrap_or(u64::MAX); + let duration = Duration::from_micros(duration); + write!(f, "{duration:?}") + } +} + +#[derive(Debug)] +pub struct BrowserMessage { + pub test_id: MessageId, + pub start_time: ElapsedMicros, +} + +impl From<&BinaryValue> for BrowserMessage { + fn from(message: &BinaryValue) -> Self { + assert_eq!(message.size(), 20); + + let mut data = vec![0; message.size()]; + message.data(Some(&mut data), 0); + + Self { + test_id: MessageId::from(&data[0..4]), + start_time: ElapsedMicros::from(&data[4..20]), + } + } +} + +impl From<&BrowserMessage> for Option { + fn from(message: &BrowserMessage) -> Self { + let mut data = vec![0; 20]; + + let test_id: [u8; 4] = (&message.test_id).into(); + let start_time: [u8; 16] = (&message.start_time).into(); + + data[0..4].copy_from_slice(&test_id); + data[4..20].copy_from_slice(&start_time); + + binary_value_create(Some(&data)) + } +} + +#[derive(Debug)] +pub struct RendererMessage { + pub test_id: MessageId, + pub duration: ElapsedMicros, + pub start_time: ElapsedMicros, +} + +impl From<&BinaryValue> for RendererMessage { + fn from(message: &BinaryValue) -> Self { + assert_eq!(message.size(), 36); + + let mut data = vec![0; message.size()]; + message.data(Some(&mut data), 0); + + Self { + test_id: MessageId::from(&data[0..4]), + duration: ElapsedMicros::from(&data[4..20]), + start_time: ElapsedMicros::from(&data[20..36]), + } + } +} + +impl From<&RendererMessage> for Option { + fn from(message: &RendererMessage) -> Self { + let mut data = vec![0; 36]; + + let test_id: [u8; 4] = (&message.test_id).into(); + let duration: [u8; 16] = (&message.duration).into(); + let start_time: [u8; 16] = (&message.start_time).into(); + + data[0..4].copy_from_slice(&test_id); + data[4..20].copy_from_slice(&duration); + data[20..36].copy_from_slice(&start_time); + + binary_value_create(Some(&data)) + } +} diff --git a/examples/tests_shared/src/common/client_app.rs b/examples/tests_shared/src/common/client_app.rs new file mode 100644 index 0000000..3d8e10e --- /dev/null +++ b/examples/tests_shared/src/common/client_app.rs @@ -0,0 +1,66 @@ +use cef::*; + +pub const PROCESS_TYPE: &str = "type"; +pub const RENDERER_PROCESS: &str = "renderer"; +#[cfg(target_os = "linux")] +pub const ZYGOTE_PROCESS: &str = "zygote"; + +pub enum ProcessType { + Browser, + Renderer, + #[cfg(target_os = "linux")] + Zygote, + Other, +} + +impl From<&CommandLine> for ProcessType { + fn from(value: &CommandLine) -> Self { + let process_type = CefString::from(PROCESS_TYPE); + if value.has_switch(Some(&process_type)) == 0 { + return Self::Browser; + } + + let value = CefString::from(&value.switch_value(Some(&process_type))).to_string(); + match value.as_str() { + RENDERER_PROCESS => Self::Renderer, + #[cfg(target_os = "linux")] + ZYGOTE_PROCESS => Self::Zygote, + _ => Self::Other, + } + } +} + +#[derive(Clone)] +pub struct ClientAppCustomScheme { + name: String, + options: i32, +} + +impl ClientAppCustomScheme { + pub fn new(name: &str, options: &[SchemeOptions]) -> Self { + let options = options.iter().fold(0, |acc, opt| acc | opt.get_raw()) as i32; + Self { + name: name.to_string(), + options, + } + } +} + +wrap_app! { + pub struct ClientApp { + custom_schemes: Vec, + } + + impl App { + fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { + let Some(registrar) = registrar else { + return; + }; + + for scheme in &self.custom_schemes { + let name = CefString::from(scheme.name.as_str()); + registrar.add_custom_scheme(Some(&name), scheme.options); + } + } + } +} diff --git a/examples/tests_shared/src/common/client_app_other.rs b/examples/tests_shared/src/common/client_app_other.rs new file mode 100644 index 0000000..4e37ebd --- /dev/null +++ b/examples/tests_shared/src/common/client_app_other.rs @@ -0,0 +1,13 @@ +use cef::*; + +wrap_app! { + pub struct ClientAppOther { + base: App, + } + + impl App { + fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { + self.base.on_register_custom_schemes(registrar); + } + } +} diff --git a/examples/tests_shared/src/common/client_switches.rs b/examples/tests_shared/src/common/client_switches.rs new file mode 100644 index 0000000..f39bf4e --- /dev/null +++ b/examples/tests_shared/src/common/client_switches.rs @@ -0,0 +1,52 @@ +//! CEF and Chromium support a wide range of command-line switches. This file +//! only contains command-line switches specific to the cefclient application. +//! View CEF/Chromium documentation or search for *_switches.cc files in the +//! Chromium source code to identify other existing command-line switches. + +pub const MULTI_THREADED_MESSAGE_LOOP: &str = "multi-threaded-message-loop"; +pub const EXTERNAL_MESSAGE_PUMP: &str = "external-message-pump"; +pub const CACHE_PATH: &str = "cache-path"; +pub const URL: &str = "url"; +pub const OFF_SCREEN_RENDERING_ENABLED: &str = "off-screen-rendering-enabled"; +pub const OFF_SCREEN_FRAME_RATE: &str = "off-screen-frame-rate"; +pub const TRANSPARENT_PAINTING_ENABLED: &str = "transparent-painting-enabled"; +pub const SHOW_UPDATE_RECT: &str = "show-update-rect"; +pub const FAKE_SCREEN_BOUNDS: &str = "fake-screen-bounds"; +pub const SHARED_TEXTURE_ENABLED: &str = "shared-texture-enabled"; +pub const EXTERNAL_BEGIN_FRAME_ENABLED: &str = "external-begin-frame-enabled"; +pub const MOUSE_CURSOR_CHANGE_DISABLED: &str = "mouse-cursor-change-disabled"; +pub const OFFLINE: &str = "offline"; +pub const FILTER_CHROME_COMMANDS: &str = "filter-chrome-commands"; +pub const REQUEST_CONTEXT_PER_BROWSER: &str = "request-context-per-browser"; +pub const REQUEST_CONTEXT_SHARED_CACHE: &str = "request-context-shared-cache"; +pub const BACKGROUND_COLOR: &str = "background-color"; +pub const ENABLE_GPU: &str = "enable-gpu"; +pub const FILTER_URL: &str = "filter-url"; +pub const USE_VIEWS: &str = "use-views"; +pub const USE_NATIVE: &str = "use-native"; +pub const HIDE_FRAME: &str = "hide-frame"; +pub const HIDE_CONTROLS: &str = "hide-controls"; +pub const HIDE_OVERLAYS: &str = "hide-overlays"; +pub const ALWAYS_ON_TOP: &str = "always-on-top"; +pub const HIDE_TOP_MENU: &str = "hide-top-menu"; +pub const SSL_CLIENT_CERTIFICATE: &str = "ssl-client-certificate"; +pub const CRL_SETS_PATH: &str = "crl-sets-path"; +pub const NO_ACTIVATE: &str = "no-activate"; +pub const SHOW_CHROME_TOOLBAR: &str = "show-chrome-toolbar"; +pub const INITIAL_SHOW_STATE: &str = "initial-show-state"; +pub const USE_DEFAULT_POPUP: &str = "use-default-popup"; +pub const USE_CLIENT_DIALOGS: &str = "use-client-dialogs"; +pub const USE_TEST_HTTP_SERVER: &str = "use-test-http-server"; +pub const SHOW_WINDOW_BUTTONS: &str = "show-window-buttons"; +pub const USE_WINDOW_MODAL_DIALOG: &str = "use-window-modal-dialog"; +pub const USE_BOTTOM_CONTROLS: &str = "use-bottom-controls"; +pub const HIDE_PIP_FRAME: &str = "hide-pip-frame"; +pub const MOVE_PIP_ENABLED: &str = "move-pip-enabled"; +pub const HIDE_CHROME_BUBBLES: &str = "hide-chrome-bubbles"; +pub const HIDE_WINDOW_ON_CLOSE: &str = "hide-window-on-close"; +pub const ACCEPTS_FIRST_MOUSE: &str = "accepts-first-mouse"; +pub const USE_ALLOY_STYLE: &str = "use-alloy-style"; +pub const USE_CHROME_STYLE_WINDOW: &str = "use-chrome-style-window"; +pub const SHOW_OVERLAY_BROWSER: &str = "show-overlay-browser"; +pub const USE_ANGLE: &str = "use-angle"; +pub const OZONE_PLATFORM: &str = "ozone-platform"; diff --git a/examples/tests_shared/src/common/mod.rs b/examples/tests_shared/src/common/mod.rs new file mode 100644 index 0000000..612571c --- /dev/null +++ b/examples/tests_shared/src/common/mod.rs @@ -0,0 +1,4 @@ +pub mod binary_value_utils; +pub mod client_app; +pub mod client_app_other; +pub mod client_switches; diff --git a/examples/tests_shared/src/lib.rs b/examples/tests_shared/src/lib.rs new file mode 100644 index 0000000..1884222 --- /dev/null +++ b/examples/tests_shared/src/lib.rs @@ -0,0 +1,6 @@ +pub mod browser; +pub mod common; +pub mod renderer; + +#[cfg(target_os = "macos")] +pub mod process_helper_mac; diff --git a/examples/tests_shared/src/process_helper_mac.rs b/examples/tests_shared/src/process_helper_mac.rs new file mode 100644 index 0000000..bfd896f --- /dev/null +++ b/examples/tests_shared/src/process_helper_mac.rs @@ -0,0 +1,44 @@ +use crate::{ + common::{client_app::*, client_app_other::*}, + renderer::client_app_renderer::*, +}; +use cef::{args::Args, *}; + +pub fn run_main( + args: Args, + custom_schemes: Vec, + app_renderer_delegates: Vec>, +) -> Result<(), i32> { + #[cfg(feature = "sandbox")] + let _sandbox = { + let mut sandbox = cef::sandbox::Sandbox::new(); + sandbox.initialize(args.as_main_args()); + sandbox + }; + + let _loader = { + let loader = library_loader::LibraryLoader::new(&std::env::current_exe().unwrap(), true); + assert!(loader.load()); + loader + }; + + let _ = api_hash(sys::CEF_API_VERSION_LAST, 0); + + let app = ClientApp::new(custom_schemes); + let mut app = match args.as_cmd_line().map(|cmd| ProcessType::from(&cmd)) { + Some(ProcessType::Renderer) => { + let app_renderer = ClientAppRenderer::new(app_renderer_delegates); + ClientAppRendererApp::new(app, app_renderer) + } + _ => ClientAppOther::new(app), + }; + + match execute_process( + Some(args.as_main_args()), + Some(&mut app), + std::ptr::null_mut(), + ) { + 0 => Ok(()), + err => Err(err), + } +} diff --git a/examples/tests_shared/src/renderer/client_app_renderer.rs b/examples/tests_shared/src/renderer/client_app_renderer.rs new file mode 100644 index 0000000..3630fb2 --- /dev/null +++ b/examples/tests_shared/src/renderer/client_app_renderer.rs @@ -0,0 +1,255 @@ +use cef::*; +use std::sync::Arc; + +pub trait Delegate: Send { + fn on_web_kit_initialized(&self, _app: &Arc) {} + + fn on_browser_created( + &self, + _app: &Arc, + _browser: Option<&Browser>, + _extra_info: Option<&DictionaryValue>, + ) { + } + + fn on_browser_destroyed(&self, _app: &Arc, _browser: Option<&Browser>) {} + + fn load_handler(&self, _app: &Arc) -> Option { + None + } + + fn on_context_created( + &self, + _app: &Arc, + _browser: Option<&Browser>, + _frame: Option<&Frame>, + _context: Option<&V8Context>, + ) { + } + + fn on_context_released( + &self, + _app: &Arc, + _browser: Option<&Browser>, + _frame: Option<&Frame>, + _context: Option<&V8Context>, + ) { + } + + fn on_uncaught_exception( + &self, + _app: &Arc, + _browser: Option<&Browser>, + _frame: Option<&Frame>, + _context: Option<&V8Context>, + _exception: Option<&V8Exception>, + _stack_trace: Option<&V8StackTrace>, + ) { + } + + fn on_focused_node_changed( + &self, + _app: &Arc, + _browser: Option<&Browser>, + _frame: Option<&Frame>, + _node: Option<&Domnode>, + ) { + } + + fn on_process_message_received( + &self, + _app: &Arc, + _browser: Option<&Browser>, + _frame: Option<&Frame>, + _source_process: ProcessId, + _message: Option<&ProcessMessage>, + ) -> i32 { + 0 + } +} + +pub struct ClientAppRenderer { + delegates: Vec>, +} + +impl ClientAppRenderer { + pub fn new(delegates: Vec>) -> Arc { + Arc::new(Self { delegates }) + } + + pub fn delegates(&self) -> &[Box] { + &self.delegates + } +} + +wrap_render_process_handler! { + struct ClientAppRendererRenderProcessHandler { + client_app_renderer: Arc, + } + + impl RenderProcessHandler { + fn on_web_kit_initialized(&self) { + for delegate in self.client_app_renderer.delegates() { + delegate.on_web_kit_initialized(&self.client_app_renderer); + } + } + + fn on_browser_created( + &self, + browser: Option<&mut Browser>, + extra_info: Option<&mut DictionaryValue>, + ) { + let browser = browser.cloned(); + let extra_info = extra_info.cloned(); + for delegate in self.client_app_renderer.delegates() { + delegate.on_browser_created( + &self.client_app_renderer, + browser.as_ref(), + extra_info.as_ref(), + ); + } + } + + fn on_browser_destroyed(&self, browser: Option<&mut Browser>) { + let browser = browser.cloned(); + for delegate in self.client_app_renderer.delegates() { + delegate.on_browser_destroyed(&self.client_app_renderer, browser.as_ref()); + } + } + + fn load_handler(&self) -> Option { + for delegate in self.client_app_renderer.delegates() { + if let Some(load_handler) = delegate.load_handler(&self.client_app_renderer) { + return Some(load_handler); + } + } + None + } + + fn on_context_created( + &self, + browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + context: Option<&mut V8Context>, + ) { + let browser = browser.cloned(); + let frame = frame.cloned(); + let context = context.cloned(); + for delegate in self.client_app_renderer.delegates() { + delegate.on_context_created( + &self.client_app_renderer, + browser.as_ref(), + frame.as_ref(), + context.as_ref(), + ); + } + } + + fn on_context_released( + &self, + browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + context: Option<&mut V8Context>, + ) { + let browser = browser.cloned(); + let frame = frame.cloned(); + let context = context.cloned(); + for delegate in self.client_app_renderer.delegates() { + delegate.on_context_released( + &self.client_app_renderer, + browser.as_ref(), + frame.as_ref(), + context.as_ref(), + ); + } + } + + fn on_uncaught_exception( + &self, + browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + context: Option<&mut V8Context>, + exception: Option<&mut V8Exception>, + stack_trace: Option<&mut V8StackTrace>, + ) { + let browser = browser.cloned(); + let frame = frame.cloned(); + let context = context.cloned(); + let exception = exception.cloned(); + let stack_trace = stack_trace.cloned(); + for delegate in self.client_app_renderer.delegates() { + delegate.on_uncaught_exception( + &self.client_app_renderer, + browser.as_ref(), + frame.as_ref(), + context.as_ref(), + exception.as_ref(), + stack_trace.as_ref(), + ); + } + } + + fn on_focused_node_changed( + &self, + browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + node: Option<&mut Domnode>, + ) { + let browser = browser.cloned(); + let frame = frame.cloned(); + let node = node.cloned(); + for delegate in self.client_app_renderer.delegates() { + delegate.on_focused_node_changed( + &self.client_app_renderer, + browser.as_ref(), + frame.as_ref(), + node.as_ref(), + ); + } + } + + fn on_process_message_received( + &self, + browser: Option<&mut Browser>, + frame: Option<&mut Frame>, + source_process: ProcessId, + message: Option<&mut ProcessMessage>, + ) -> i32 { + let browser = browser.cloned(); + let frame = frame.cloned(); + let message = message.cloned(); + for delegate in self.client_app_renderer.delegates() { + let handled = delegate.on_process_message_received( + &self.client_app_renderer, + browser.as_ref(), + frame.as_ref(), + source_process, + message.as_ref(), + ); + if handled != 0 { + return handled; + } + } + 0 + } + } +} + +wrap_app! { + pub struct ClientAppRendererApp { + base: App, + client_app_renderer: Arc, + } + + impl App { + fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) { + self.base.on_register_custom_schemes(registrar); + } + + fn render_process_handler(&self) -> Option { + Some(ClientAppRendererRenderProcessHandler::new( + self.client_app_renderer.clone(), + )) + } + } +} diff --git a/examples/tests_shared/src/renderer/mod.rs b/examples/tests_shared/src/renderer/mod.rs new file mode 100644 index 0000000..933bf10 --- /dev/null +++ b/examples/tests_shared/src/renderer/mod.rs @@ -0,0 +1 @@ +pub mod client_app_renderer; diff --git a/export-cef-dir/src/main.rs b/export-cef-dir/src/main.rs index 943990c..b439049 100644 --- a/export-cef-dir/src/main.rs +++ b/export-cef-dir/src/main.rs @@ -19,7 +19,7 @@ fn default_version() -> &'static str { fn default_download_url() -> &'static str { static DEFAULT_DOWNLOAD_URL: OnceLock = OnceLock::new(); DEFAULT_DOWNLOAD_URL - .get_or_init(|| download_cef::default_download_url()) + .get_or_init(download_cef::default_download_url) .as_str() } diff --git a/get-latest/src/main.rs b/get-latest/src/main.rs index f060b18..482c960 100644 --- a/get-latest/src/main.rs +++ b/get-latest/src/main.rs @@ -44,7 +44,7 @@ type Result = std::result::Result; fn default_download_url() -> &'static str { static DEFAULT_DOWNLOAD_URL: OnceLock = OnceLock::new(); DEFAULT_DOWNLOAD_URL - .get_or_init(|| download_cef::default_download_url()) + .get_or_init(download_cef::default_download_url) .as_str() } diff --git a/update-bindings/src/main.rs b/update-bindings/src/main.rs index 03baad0..a95a29c 100644 --- a/update-bindings/src/main.rs +++ b/update-bindings/src/main.rs @@ -41,7 +41,7 @@ fn default_version() -> &'static str { fn default_download_url() -> &'static str { static DEFAULT_DOWNLOAD_URL: OnceLock = OnceLock::new(); DEFAULT_DOWNLOAD_URL - .get_or_init(|| download_cef::default_download_url()) + .get_or_init(download_cef::default_download_url) .as_str() } diff --git a/update-bindings/src/parse_tree.rs b/update-bindings/src/parse_tree.rs index 1544827..922ee18 100644 --- a/update-bindings/src/parse_tree.rs +++ b/update-bindings/src/parse_tree.rs @@ -2720,6 +2720,7 @@ fn make_my_struct() -> {rust_name} {{ $($generic_type: $first_generic_type_bound $(+ $generic_type_bound)*,)+ )? { + #[allow(clippy::new_ret_no_self)] pub fn new($($field_name: $field_type),*) -> #rust_name { #rust_name::new( Self {