mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-04-03 10:11:15 +02:00
wip init scripts
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -9016,6 +9016,8 @@ dependencies = [
|
||||
"gtk",
|
||||
"http 1.3.1",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-runtime",
|
||||
"tauri-utils",
|
||||
"url",
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user