wip init scripts

This commit is contained in:
Lucas Nogueira
2025-10-31 18:46:33 -03:00
parent 3013a14ee8
commit f59495ea6d
6 changed files with 424 additions and 37 deletions

2
Cargo.lock generated
View File

@@ -9016,6 +9016,8 @@ dependencies = [
"gtk",
"http 1.3.1",
"raw-window-handle",
"serde",
"serde_json",
"tauri-runtime",
"tauri-utils",
"url",

View File

@@ -17,6 +17,8 @@ url = "2"
http = "1"
cef = "141.6.0"
cef-dll-sys = "141.6.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies]
gtk = { version = "0.18", features = ["v3_24"] }

View File

@@ -4,11 +4,11 @@ use std::{
collections::HashMap,
sync::{
atomic::{AtomicU32, Ordering},
Arc,
Arc, Mutex, OnceLock,
},
};
use tauri_runtime::{
webview::UriSchemeProtocol,
webview::{InitializationScript, UriSchemeProtocol},
window::{PendingWindow, WindowId},
RunEvent, UserEvent,
};
@@ -17,6 +17,43 @@ use crate::{AppWindow, CefRuntime, Message};
mod request_handler;
// Message name for process messages containing initialization scripts
const INIT_SCRIPTS_MESSAGE_NAME: &str = "tauri_init_scripts";
// Serializable version of InitializationScript for process messages
#[derive(serde::Serialize, serde::Deserialize)]
struct SerializableInitScript {
script: String,
for_main_frame_only: bool,
}
impl From<&InitializationScript> for SerializableInitScript {
fn from(script: &InitializationScript) -> Self {
Self {
script: script.script.clone(),
for_main_frame_only: script.for_main_frame_only,
}
}
}
impl From<SerializableInitScript> for InitializationScript {
fn from(script: SerializableInitScript) -> Self {
Self {
script: script.script,
for_main_frame_only: script.for_main_frame_only,
}
}
}
// Static registry in render process to store initialization scripts received via process messages
// This is populated when the browser process sends scripts via process messages
static RENDER_INIT_SCRIPTS_REGISTRY: OnceLock<Mutex<HashMap<i32, Vec<InitializationScript>>>> =
OnceLock::new();
fn get_render_init_scripts_registry() -> &'static Mutex<HashMap<i32, Vec<InitializationScript>>> {
RENDER_INIT_SCRIPTS_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
}
#[derive(Clone)]
pub struct Context<T: UserEvent> {
pub windows: Arc<RefCell<HashMap<WindowId, AppWindow>>>,
@@ -25,6 +62,9 @@ pub struct Context<T: UserEvent> {
pub next_webview_id: Arc<AtomicU32>,
pub next_window_event_id: Arc<AtomicU32>,
pub next_webview_event_id: Arc<AtomicU32>,
/// Initialization scripts stored per browser ID
/// Scripts are stored here and then registered to shared registry when browser is available
pub initialization_scripts: Arc<Mutex<HashMap<i32, Vec<InitializationScript>>>>,
}
impl<T: UserEvent> Context<T> {
@@ -54,6 +94,178 @@ wrap_app! {
fn browser_process_handler(&self) -> Option<BrowserProcessHandler> {
Some(AppBrowserProcessHandler::new(self.context.clone()))
}
fn render_process_handler(&self) -> Option<RenderProcessHandler> {
Some(AppRenderProcessHandler::new(self.context.clone()))
}
/// Called before the process starts to register custom schemes.
/// This is where we mark schemes as fetch-enabled, secure, and CORS-enabled.
fn on_register_custom_schemes(&self, registrar: Option<&mut SchemeRegistrar>) {
if let Some(registrar) = registrar {
// Standard CEF scheme options for custom protocols:
// - FETCH_ENABLED: Allows Fetch API requests
// - SECURE: Treats as secure like https (no mixed content warnings)
// - CORS_ENABLED: Allows CORS requests
// - STANDARD: Standard URL scheme behavior
let scheme_options = (cef_dll_sys::cef_scheme_options_t::CEF_SCHEME_OPTION_FETCH_ENABLED as i32)
| (cef_dll_sys::cef_scheme_options_t::CEF_SCHEME_OPTION_SECURE as i32)
| (cef_dll_sys::cef_scheme_options_t::CEF_SCHEME_OPTION_CORS_ENABLED as i32)
| (cef_dll_sys::cef_scheme_options_t::CEF_SCHEME_OPTION_STANDARD as i32);
for scheme in ["ipc", "tauri"] {
let scheme_name = CefString::from(scheme);
registrar.add_custom_scheme(Some(&scheme_name), scheme_options);
}
}
}
}
}
wrap_render_process_handler! {
struct AppRenderProcessHandler<T: UserEvent> {
context: Context<T>,
}
impl RenderProcessHandler {
/// Called when a process message is received from the browser process.
/// We use this to receive initialization scripts.
fn on_process_message_received(
&self,
_browser: Option<&mut Browser>,
_frame: Option<&mut Frame>,
source_process: ProcessId,
message: Option<&mut ProcessMessage>,
) -> ::std::os::raw::c_int {
// Only handle messages from browser process
if source_process.as_ref() != &cef_dll_sys::cef_process_id_t::PID_BROWSER {
return 0;
}
let Some(message) = message else { return 0 };
// Check if this is our initialization scripts message
let msg_name = message.name();
let msg_name_str = {
let name = CefString::from(&msg_name).to_string();
name
};
if msg_name_str != INIT_SCRIPTS_MESSAGE_NAME {
return 0;
}
// Extract browser_id and scripts from message arguments
let Some(args) = message.argument_list() else { return 0 };
// First argument: browser_id (int)
let browser_id = args.int(0);
// Second argument: scripts as JSON string
let scripts_json = {
let str_free = args.string(1);
CefString::from(&str_free).to_string()
};
// Deserialize scripts
if let Ok(serializable_scripts) = serde_json::from_str::<Vec<SerializableInitScript>>(&scripts_json) {
let scripts: Vec<InitializationScript> = serializable_scripts
.into_iter()
.map(InitializationScript::from)
.collect();
get_render_init_scripts_registry()
.lock()
.unwrap()
.insert(browser_id, scripts);
}
1 // Message handled
}
/// Called when a new V8 JavaScript context is created.
/// This happens:
/// - When the browser first loads a page
/// - When navigating to a new page (context is recreated)
/// - When navigating in iframes (each frame gets its own context)
///
/// This ensures initialization scripts run before page scripts on every navigation.
fn on_context_created(
&self,
browser: Option<&mut Browser>,
frame: Option<&mut Frame>,
context: Option<&mut V8Context>,
) {
let Some(frame) = frame else { return };
let Some(context) = context else { return };
let Some(browser) = browser else { return };
// Get browser ID to look up initialization scripts
let browser_id = browser.identifier();
// Check if this is the main frame
let is_main_frame = frame.is_main() == 1;
// Get initialization scripts for this browser from render process registry
// Scripts are received via process messages from the browser process
let scripts = {
get_render_init_scripts_registry()
.lock()
.unwrap()
.get(&browser_id)
.cloned()
.unwrap_or_default()
};
if scripts.is_empty() {
return;
}
// Enter the V8 context so we can execute scripts synchronously
context.enter();
// Get the frame URL for proper script execution context
let frame_url = {
let url_free = frame.url();
let url = CefString::from(&url_free).to_string();
url
};
// Inject each initialization script directly into the V8 context
// Use V8Context::eval for synchronous execution - this runs immediately,
// before any page scripts can execute
for script in &scripts {
// If script is for main frame only, skip if this is not the main frame
if script.for_main_frame_only && !is_main_frame {
continue;
}
// Execute the script synchronously in the V8 context
// This runs immediately before any page scripts can execute
let script_str = CefString::from(script.script.as_str());
let url_str = CefString::from(frame_url.as_str());
let mut retval = None;
let mut exception = None;
// eval returns 1 on success, 0 on failure
let result = context.eval(
Some(&script_str),
Some(&url_str),
0,
Some(&mut retval),
Some(&mut exception),
);
if result == 0 {
// Script execution failed - log exception if available
if let Some(exc) = exception {
let msg = exc.message();
eprintln!("Failed to execute initialization script: {}", CefString::from(&msg).to_string());
}
}
}
// Exit the V8 context
context.exit();
}
}
}
@@ -71,12 +283,108 @@ wrap_browser_process_handler! {
}
wrap_client! {
struct BrowserClient;
struct BrowserClient<T: UserEvent> {
context: Context<T>,
pending_scripts: Vec<InitializationScript>,
}
impl Client {
fn request_handler(&self) -> Option<RequestHandler> {
Some(request_handler::WebRequestHandler::new())
}
fn load_handler(&self) -> Option<LoadHandler> {
Some(LoadHandlerImpl::new(
self.context.clone(),
self.pending_scripts.clone(),
Arc::new(Mutex::new(false)),
))
}
fn life_span_handler(&self) -> Option<LifeSpanHandler> {
Some(LifeSpanHandlerImpl::new(
self.context.clone(),
self.pending_scripts.clone(),
))
}
}
}
wrap_life_span_handler! {
struct LifeSpanHandlerImpl<T: UserEvent> {
context: Context<T>,
pending_scripts: Vec<InitializationScript>,
}
impl LifeSpanHandler {
/// Called after the browser is created.
/// Send initialization scripts to render process immediately so they're available
/// when OnContextCreated is called.
fn on_after_created(&self, browser: Option<&mut Browser>) {
if let Some(browser) = browser {
let browser_id = browser.identifier();
// Store scripts in Context (browser process)
if !self.pending_scripts.is_empty() {
// Store in Context for browser process access
self.context
.initialization_scripts
.lock()
.unwrap()
.insert(browser_id, self.pending_scripts.clone());
// Send scripts to render process via process message
// Do this as early as possible so scripts are available before OnContextCreated
if let Some(main_frame) = browser.main_frame() {
// Convert to serializable format and serialize to JSON
let serializable_scripts: Vec<SerializableInitScript> = self
.pending_scripts
.iter()
.map(SerializableInitScript::from)
.collect();
if let Ok(scripts_json) = serde_json::to_string(&serializable_scripts) {
// Create process message
let mut msg = process_message_create(Some(&CefString::from(INIT_SCRIPTS_MESSAGE_NAME)))
.expect("Failed to create process message");
// Set arguments: browser_id and scripts JSON
if let Some(args) = msg.argument_list() {
args.set_int(0, browser_id);
args.set_string(1, Some(&CefString::from(scripts_json.as_str())));
// Send to render process
main_frame.send_process_message(
ProcessId::from(cef_dll_sys::cef_process_id_t::PID_RENDERER),
Some(&mut msg),
);
}
}
}
}
}
}
}
}
wrap_load_handler! {
struct LoadHandlerImpl<T: UserEvent> {
context: Context<T>,
pending_scripts: Vec<InitializationScript>,
scripts_registered: Arc<Mutex<bool>>,
}
impl LoadHandler {
fn on_loading_state_change(
&self,
browser: Option<&mut Browser>,
_is_loading: ::std::os::raw::c_int,
_can_go_back: ::std::os::raw::c_int,
_can_go_forward: ::std::os::raw::c_int,
) {
// Scripts are now sent in LifeSpanHandler::on_after_created (earlier),
// so we don't need to send them here anymore
}
}
}
@@ -168,7 +476,10 @@ fn create_window<T: UserEvent>(
let webview = pending.webview.unwrap();
let mut client = BrowserClient::new();
// Get initialization scripts from webview attributes
let initialization_scripts = webview.webview_attributes.initialization_scripts.clone();
let mut client = BrowserClient::new(context.clone(), initialization_scripts.clone());
let url = CefString::from(webview.url.as_str());
let global_context =
@@ -183,6 +494,10 @@ fn create_window<T: UserEvent>(
Option::<&mut RequestContextHandler>::None,
);
if let Some(request_context) = &request_context {
// Ensure schemes are registered with proper flags (fetch-enabled, secure, etc.)
// This is done in App::on_register_custom_schemes, but we also track
// which schemes we've seen
for (scheme, handler) in webview.uri_scheme_protocols {
let label = label.clone();
request_context.register_scheme_handler_factory(

View File

@@ -95,11 +95,15 @@ wrap_resource_handler! {
let label = self.context.label.clone();
let handler = self.context.handler.clone();
let data = read_request_body(request);
let headers = get_request_headers(request);
let method_str = CefString::from(&request.method()).to_string();
let method = http::Method::from_bytes(method_str.as_bytes())
.unwrap_or(http::Method::GET);
std::thread::spawn(move || {
let mut http_request = http::Request::builder().uri(url.as_str()).body(data).unwrap();
let mut http_request = http::Request::builder().method(method).uri(url.as_str()).body(data).unwrap();
*http_request.headers_mut() = headers;
(handler)(&label, http_request, responder);
});

View File

@@ -1082,6 +1082,7 @@ impl<T: UserEvent> CefRuntime<T> {
next_webview_id: Default::default(),
next_window_id: Default::default(),
next_window_event_id: Default::default(),
initialization_scripts: Arc::new(Mutex::new(HashMap::new())),
};
let mut app = cef_impl::TauriApp::new(cef_context.clone());
@@ -1097,11 +1098,8 @@ impl<T: UserEvent> CefRuntime<T> {
);
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
std::process::exit(0);

View File

@@ -22,6 +22,7 @@
function sendIpcMessage(message) {
const { cmd, callback, error, payload, options } = message
console.log(customProtocolIpcFailed, canUseCustomProtocol, cmd)
if (
!customProtocolIpcFailed
&& (canUseCustomProtocol || cmd === fetchChannelDataCommand)
@@ -34,39 +35,104 @@
headers.set('Tauri-Error', error)
headers.set('Tauri-Invoke-Key', __TAURI_INVOKE_KEY__)
fetch(window.__TAURI_INTERNALS__.convertFileSrc(cmd, 'ipc'), {
method: 'POST',
body: data,
headers
})
.then((response) => {
const callbackId =
response.headers.get('Tauri-Response') === 'ok' ? callback : error
// we need to split here because on Android the content-type gets duplicated
switch ((response.headers.get('content-type') || '').split(',')[0]) {
case 'application/json':
return response.json().then((r) => [callbackId, r])
case 'text/plain':
return response.text().then((r) => [callbackId, r])
default:
return response.arrayBuffer().then((r) => [callbackId, r])
// For CEF runtime, try XMLHttpRequest first as it may populate PostData correctly
// Fetch API has a known issue with custom schemes in CEF - XMLHttpRequest may work
// Detect CEF: check for CEF-specific properties or user agent
const useXHR = typeof window.cef !== 'undefined' ||
navigator.userAgent.includes('CEF') ||
navigator.userAgent.includes('Chromium Embedded')
if (useXHR) {
// Try XMLHttpRequest for CEF - it may handle POST bodies to custom schemes correctly
try {
const xhr = new XMLHttpRequest()
const url = window.__TAURI_INTERNALS__.convertFileSrc(cmd, 'ipc')
xhr.open('POST', url, true)
// Set headers
headers.forEach((value, key) => {
xhr.setRequestHeader(key, value)
})
xhr.onload = function () {
const callbackId = xhr.getResponseHeader('Tauri-Response') === 'ok' ? callback : error
const contentType = (xhr.getResponseHeader('content-type') || '').split(',')[0]
let responseData
switch (contentType) {
case 'application/json':
try {
responseData = JSON.parse(xhr.responseText)
} catch {
responseData = xhr.responseText
}
break
case 'text/plain':
responseData = xhr.responseText
break
default:
responseData = xhr.response
break
}
window.__TAURI_INTERNALS__.runCallback(callbackId, responseData)
}
})
.then(
([callbackId, data]) => {
window.__TAURI_INTERNALS__.runCallback(callbackId, data)
},
(e) => {
xhr.onerror = function () {
console.warn(
'IPC custom protocol failed, Tauri will now use the postMessage interface instead',
e
'IPC custom protocol failed with XMLHttpRequest, trying Fetch API',
xhr.status
)
// failed to use the custom protocol IPC (either the webview blocked a custom protocol or it was a CSP error)
// so we need to fallback to the postMessage interface
customProtocolIpcFailed = true
sendIpcMessage(message)
// Fall through to Fetch API
sendWithFetch()
}
)
xhr.send(data)
return
} catch (e) {
console.warn('XMLHttpRequest failed, falling back to Fetch API', e)
// Fall through to Fetch API
}
}
function sendWithFetch() {
fetch(window.__TAURI_INTERNALS__.convertFileSrc(cmd, 'ipc'), {
method: 'POST',
body: data,
headers
})
.then((response) => {
const callbackId =
response.headers.get('Tauri-Response') === 'ok' ? callback : error
// we need to split here because on Android the content-type gets duplicated
switch ((response.headers.get('content-type') || '').split(',')[0]) {
case 'application/json':
return response.json().then((r) => [callbackId, r])
case 'text/plain':
return response.text().then((r) => [callbackId, r])
default:
return response.arrayBuffer().then((r) => [callbackId, r])
}
})
.then(
([callbackId, data]) => {
window.__TAURI_INTERNALS__.runCallback(callbackId, data)
},
(e) => {
console.warn(
'IPC custom protocol failed, Tauri will now use the postMessage interface instead',
e
)
// failed to use the custom protocol IPC (either the webview blocked a custom protocol or it was a CSP error)
// so we need to fallback to the postMessage interface
customProtocolIpcFailed = true
sendIpcMessage(message)
}
)
}
sendWithFetch()
} else {
// otherwise use the postMessage interface
const { data } = processIpcMessage({