diff --git a/.changes/tracing.md b/.changes/tracing.md new file mode 100644 index 000000000..27ccb5b2b --- /dev/null +++ b/.changes/tracing.md @@ -0,0 +1,5 @@ +--- +"wry": patch +--- + +Added tracing spans for `evaluate_script`, `ipc_handler` and `custom_protocols` behind the `tracing` feature flag. diff --git a/Cargo.toml b/Cargo.toml index e583bab87..4b7a7b8c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ os-webview = [ "x11-dl", "gdkx11" ] +tracing = [ "dep:tracing" ] [build-dependencies] cfg_aliases = "0.1" @@ -50,6 +51,7 @@ cfg_aliases = "0.1" [dependencies] libc = "0.2" log = "0.4" +tracing = { version = "0.1", optional = true } once_cell = "1" serde = { version = "1.0", features = [ "derive" ] } serde_json = "1.0" diff --git a/src/android/binding.rs b/src/android/binding.rs index c227e0653..e4a352748 100644 --- a/src/android/binding.rs +++ b/src/android/binding.rs @@ -10,13 +10,13 @@ use jni::errors::Result as JniResult; pub use jni::{ self, objects::{GlobalRef, JClass, JMap, JObject, JString}, - sys::{jboolean, jobject, jstring}, + sys::{jboolean, jint, jobject, jstring}, JNIEnv, }; pub use ndk; use super::{ - ASSET_LOADER_DOMAIN, IPC, ON_LOAD_HANDLER, REQUEST_HANDLER, TITLE_CHANGE_HANDLER, + ASSET_LOADER_DOMAIN, EVAL_CALLBACKS, IPC, ON_LOAD_HANDLER, REQUEST_HANDLER, TITLE_CHANGE_HANDLER, URL_LOADING_OVERRIDE, WITH_ASSET_LOADER, }; @@ -72,6 +72,7 @@ macro_rules! android_binding { [JString], jboolean ); + android_fn!($domain, $package, RustWebView, onEval, [jint, JString]); android_fn!( $domain, $package, @@ -103,6 +104,10 @@ fn handle_request( is_document_start_script_enabled: jboolean, ) -> JniResult { if let Some(handler) = REQUEST_HANDLER.get() { + #[cfg(feature = "tracing")] + let span = + tracing::info_span!("wry::custom_protocol::handle", uri = tracing::field::Empty).entered(); + let mut request_builder = Request::builder(); let uri = env @@ -112,7 +117,12 @@ fn handle_request( .call_method(&uri, "toString", "()Ljava/lang/String;", &[])? .l()? .into(); - request_builder = request_builder.uri(&env.get_string(&url)?.to_string_lossy().to_string()); + let url = env.get_string(&url)?.to_string_lossy().to_string(); + + #[cfg(feature = "tracing")] + span.record("uri", &url); + + request_builder = request_builder.uri(&url); let method = env .call_method(&request, "getMethod", "()Ljava/lang/String;", &[])? @@ -152,7 +162,11 @@ fn handle_request( } }; - let response = (handler.handler)(final_request, is_document_start_script_enabled != 0); + let response = { + #[cfg(feature = "tracing")] + let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered(); + (handler.handler)(final_request, is_document_start_script_enabled != 0) + }; if let Some(response) = response { let status = response.status(); let status_code = status.as_u16() as i32; @@ -267,9 +281,31 @@ pub unsafe fn shouldOverride(mut env: JNIEnv, _: JClass, url: JString) -> jboole .into() } +#[allow(non_snake_case)] +pub unsafe fn onEval(mut env: JNIEnv, _: JClass, id: jint, result: JString) { + match env.get_string(&result) { + Ok(result) => { + if let Some(cb) = EVAL_CALLBACKS + .get_or_init(Default::default) + .lock() + .unwrap() + .get(&id) + { + cb(result.into()); + } + } + Err(e) => { + log::warn!("Failed to parse JString: {}", e); + } + } +} + pub unsafe fn ipc(mut env: JNIEnv, _: JClass, arg: JString) { match env.get_string(&arg) { Ok(arg) => { + #[cfg(feature = "tracing")] + let _span = tracing::info_span!("wry::ipc::handle").entered(); + let arg = arg.to_string_lossy().to_string(); if let Some(ipc) = IPC.get() { (ipc.handler)(arg) diff --git a/src/android/kotlin/RustWebView.kt b/src/android/kotlin/RustWebView.kt index 354349464..aef4b9d94 100644 --- a/src/android/kotlin/RustWebView.kt +++ b/src/android/kotlin/RustWebView.kt @@ -67,6 +67,14 @@ class RustWebView(context: Context, val initScripts: Array): WebView(con } } + fun evalScript(id: Int, script: String) { + post { + super.evaluateJavascript(script) { result -> + onEval(id, result) + } + } + } + fun clearAllBrowsingData() { try { super.getContext().deleteDatabase("webviewCache.db") @@ -90,6 +98,7 @@ class RustWebView(context: Context, val initScripts: Array): WebView(con } private external fun shouldOverride(url: String): Boolean + private external fun onEval(id: Int, result: String) {{class-extension}} } diff --git a/src/android/main_pipe.rs b/src/android/main_pipe.rs index 408164367..3d6cc0b3f 100644 --- a/src/android/main_pipe.rs +++ b/src/android/main_pipe.rs @@ -10,9 +10,9 @@ use jni::{ JNIEnv, }; use once_cell::sync::Lazy; -use std::os::unix::prelude::*; +use std::{os::unix::prelude::*, sync::atomic::Ordering}; -use super::{find_class, PACKAGE}; +use super::{find_class, EvalCallback, EVAL_CALLBACKS, EVAL_ID_GENERATOR, PACKAGE}; static CHANNEL: Lazy<(Sender, Receiver)> = Lazy::new(|| bounded(8)); pub static MAIN_PIPE: Lazy<[RawFd; 2]> = Lazy::new(|| { @@ -194,14 +194,39 @@ impl<'a> MainPipe<'a> { self.webview = Some(webview); } - WebViewMessage::Eval(script) => { + WebViewMessage::Eval(script, callback) => { if let Some(webview) = &self.webview { + let id = EVAL_ID_GENERATOR + .get_or_init(Default::default) + .fetch_add(1, Ordering::Relaxed); + + #[cfg(feature = "tracing")] + let span = std::sync::Mutex::new(Some(SendEnteredSpan( + tracing::debug_span!("wry::eval").entered(), + ))); + + EVAL_CALLBACKS + .get_or_init(Default::default) + .lock() + .unwrap() + .insert( + id, + Box::new(move |result| { + #[cfg(feature = "tracing")] + span.lock().unwrap().take(); + + if let Some(callback) = &callback { + callback(result); + } + }), + ); + let s = self.env.new_string(script)?; self.env.call_method( webview.as_obj(), - "evaluateJavascript", - "(Ljava/lang/String;Landroid/webkit/ValueCallback;)V", - &[(&s).into(), JObject::null().as_ref().into()], + "evalScript", + "(ILjava/lang/String;)V", + &[id.into(), (&s).into()], )?; } } @@ -340,7 +365,7 @@ fn set_background_color<'a>( pub(crate) enum WebViewMessage { CreateWebView(CreateWebViewAttributes), - Eval(String), + Eval(String, Option), SetBackgroundColor(RGBA), GetWebViewVersion(Sender>), GetUrl(Sender), @@ -362,3 +387,10 @@ pub(crate) struct CreateWebViewAttributes { pub user_agent: Option, pub initialization_scripts: Vec, } + +// SAFETY: only use this when you are sure the span will be dropped on the same thread it was entered +#[cfg(feature = "tracing")] +struct SendEnteredSpan(tracing::span::EnteredSpan); + +#[cfg(feature = "tracing")] +unsafe impl Send for SendEnteredSpan {} diff --git a/src/android/mod.rs b/src/android/mod.rs index 98c9a32e5..41213bef2 100644 --- a/src/android/mod.rs +++ b/src/android/mod.rs @@ -20,7 +20,11 @@ use kuchiki::NodeRef; use ndk::looper::{FdEvent, ForeignLooper}; use once_cell::sync::OnceCell; use sha2::{Digest, Sha256}; -use std::{borrow::Cow, sync::mpsc::channel}; +use std::{ + borrow::Cow, + collections::HashMap, + sync::{atomic::AtomicI32, mpsc::channel, Mutex}, +}; use url::Url; pub(crate) mod binding; @@ -64,6 +68,12 @@ pub static ASSET_LOADER_DOMAIN: OnceCell = OnceCell::new(); pub(crate) static PACKAGE: OnceCell = OnceCell::new(); +type EvalCallback = Box; + +pub static EVAL_ID_GENERATOR: OnceCell = OnceCell::new(); +pub static EVAL_CALLBACKS: once_cell::sync::OnceCell>> = + once_cell::sync::OnceCell::new(); + /// Sets up the necessary logic for wry to be able to create the webviews later. pub unsafe fn android_setup( package: &str, @@ -299,8 +309,11 @@ impl InnerWebView { Url::parse(uri.as_str()).unwrap() } - pub fn eval(&self, js: &str, _callback: Option) -> Result<()> { - MainPipe::send(WebViewMessage::Eval(js.into())); + pub fn eval(&self, js: &str, callback: Option) -> Result<()> { + MainPipe::send(WebViewMessage::Eval( + js.into(), + callback.map(|c| Box::new(c) as Box), + )); Ok(()) } @@ -344,7 +357,7 @@ impl InnerWebView { } } - pub fn set_bounds(&self, bounds: crate::Rect) { + pub fn set_bounds(&self, _bounds: crate::Rect) { // Unsupported } diff --git a/src/lib.rs b/src/lib.rs index aa4297d93..d9b5a91f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -160,6 +160,7 @@ //! libraries and prevent from building documentation on doc.rs fails. //! - `linux-body`: Enables body support of custom protocol request on Linux. Requires //! webkit2gtk v2.40 or above. +//! - `tracing`: enables [tracing] for `evaluate_script`, `ipc_handler` and `custom_protocols. //! //! [`tao`]: https://docs.rs/tao //! [`winit`]: https://docs.rs/winit diff --git a/src/webkitgtk/mod.rs b/src/webkitgtk/mod.rs index db591c95b..ebd62b388 100644 --- a/src/webkitgtk/mod.rs +++ b/src/webkitgtk/mod.rs @@ -238,6 +238,9 @@ impl InnerWebView { // Connect before registering as recommended by the docs manager.connect_script_message_received(None, move |_m, msg| { + #[cfg(feature = "tracing")] + let _span = tracing::info_span!("wry::ipc::handle").entered(); + if let Some(js) = msg.js_value() { if let Some(ipc_handler) = &ipc_handler { ipc_handler(js.to_string()); @@ -487,24 +490,27 @@ impl InnerWebView { } else { let cancellable: Option<&Cancellable> = None; - match callback { - Some(callback) => { - self.webview.run_javascript(js, cancellable, |result| { - let mut result_str = String::new(); + #[cfg(feature = "tracing")] + let span = SendEnteredSpan(tracing::debug_span!("wry::eval").entered()); - if let Ok(js_result) = result { - if let Some(js_value) = js_result.js_value() { - if let Some(json_str) = js_value.to_json(0) { - result_str = json_str.to_string(); - } + self.webview.run_javascript(js, cancellable, |result| { + #[cfg(feature = "tracing")] + drop(span); + + if let Some(callback) = callback { + let mut result_str = String::new(); + + if let Ok(js_result) = result { + if let Some(js_value) = js_result.js_value() { + if let Some(json_str) = js_value.to_json(0) { + result_str = json_str.to_string(); } } + } - callback(result_str); - }); + callback(result_str); } - None => self.webview.run_javascript(js, cancellable, |_| ()), - }; + }); } Ok(()) @@ -700,3 +706,10 @@ pub fn platform_webview_version() -> Result { }; Ok(format!("{}.{}.{}", major, minor, patch)) } + +// SAFETY: only use this when you are sure the span will be dropped on the same thread it was entered +#[cfg(feature = "tracing")] +struct SendEnteredSpan(tracing::span::EnteredSpan); + +#[cfg(feature = "tracing")] +unsafe impl Send for SendEnteredSpan {} diff --git a/src/webkitgtk/web_context.rs b/src/webkitgtk/web_context.rs index 30bba6c69..ed77cb14a 100644 --- a/src/webkitgtk/web_context.rs +++ b/src/webkitgtk/web_context.rs @@ -296,9 +296,16 @@ where .register_uri_scheme_as_secure(name); context.register_uri_scheme(name, move |request| { + #[cfg(feature = "tracing")] + let span = + tracing::info_span!("wry::custom_protocol::handle", uri = tracing::field::Empty).entered(); + if let Some(uri) = request.uri() { let uri = uri.as_str(); + #[cfg(feature = "tracing")] + span.record("uri", uri); + // FIXME: Read the body (forms post) #[allow(unused_mut)] let mut http_request = Request::builder().uri(uri).method("GET"); @@ -393,6 +400,8 @@ where request_.finish_with_response(&response); }); + #[cfg(feature = "tracing")] + let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered(); handler(http_request, RequestAsyncResponder { responder }); } else { request.finish_error(&mut glib::Error::new( diff --git a/src/webview2/mod.rs b/src/webview2/mod.rs index aab68b5f4..1de48baec 100644 --- a/src/webview2/mod.rs +++ b/src/webview2/mod.rs @@ -477,6 +477,8 @@ impl InnerWebView { args.TryGetWebMessageAsString(&mut js)?; let js = take_pwstr(js); if let Some(ipc_handler) = &ipc_handler { + #[cfg(feature = "tracing")] + let _span = tracing::info_span!("wry::ipc::handle").entered(); ipc_handler(js); } } @@ -635,6 +637,10 @@ impl InnerWebView { webview .add_WebResourceRequested( &WebResourceRequestedEventHandler::create(Box::new(move |_, args| { + #[cfg(feature = "tracing")] + let span = + tracing::info_span!("wry::custom_protocol::handle", uri = tracing::field::Empty) + .entered(); if let Some(args) = args { let webview_request = args.Request()?; let mut request = Request::builder(); @@ -691,6 +697,9 @@ impl InnerWebView { webview_request.Uri(&mut uri)?; let uri = take_pwstr(uri); + #[cfg(feature = "tracing")] + span.record("uri", &uri); + if let Some(custom_protocol) = custom_protocols .iter() .find(|(name, _)| uri.starts_with(&format!("{scheme}://{name}."))) @@ -747,6 +756,8 @@ impl InnerWebView { } }); + #[cfg(feature = "tracing")] + let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered(); (custom_protocol.1)(final_request, RequestAsyncResponder { responder }); return Ok(()); } @@ -926,9 +937,13 @@ impl InnerWebView { callback: impl FnOnce(String) + Send + 'static, ) -> windows::core::Result<()> { unsafe { + #[cfg(feature = "tracing")] + let span = tracing::debug_span!("wry::eval").entered(); webview.ExecuteScript( PCWSTR::from_raw(encode_wide(js).as_ptr()), &ExecuteScriptCompletedHandler::create(Box::new(|_, return_str| { + #[cfg(feature = "tracing")] + drop(span); callback(return_str); Ok(()) })), diff --git a/src/wkwebview/mod.rs b/src/wkwebview/mod.rs index 6fbcfe18d..9be5703cd 100644 --- a/src/wkwebview/mod.rs +++ b/src/wkwebview/mod.rs @@ -136,6 +136,9 @@ impl InnerWebView { extern "C" fn did_receive(this: &Object, _: Sel, _: id, msg: id) { // Safety: objc runtime calls are unsafe unsafe { + #[cfg(feature = "tracing")] + let _span = tracing::info_span!("wry::ipc::handle").entered(); + let function = this.get_ivar::<*mut c_void>("function"); if !function.is_null() { let function = &mut *(*function as *mut Box); @@ -157,6 +160,9 @@ impl InnerWebView { // Task handler for custom protocol extern "C" fn start_task(this: &Object, _: Sel, _webview: id, task: id) { unsafe { + #[cfg(feature = "tracing")] + let span = tracing::info_span!("wry::custom_protocol::handle", uri = tracing::field::Empty) + .entered(); let function = this.get_ivar::<*mut c_void>("function"); if !function.is_null() { let function = @@ -166,10 +172,14 @@ impl InnerWebView { let request: id = msg_send![task, request]; let url: id = msg_send![request, URL]; - let nsstring = { + let uri_nsstring = { let s: id = msg_send![url, absoluteString]; NSString(s) }; + let uri = uri_nsstring.to_str(); + + #[cfg(feature = "tracing")] + span.record("uri", uri); // Get request method (GET, POST, PUT etc...) let method = { @@ -178,9 +188,7 @@ impl InnerWebView { }; // Prepare our HttpRequest - let mut http_request = Request::builder() - .uri(nsstring.to_str()) - .method(method.to_str()); + let mut http_request = Request::builder().uri(uri).method(method.to_str()); // Get body let mut sent_form_body = Vec::new(); @@ -266,6 +274,8 @@ impl InnerWebView { }, ); + #[cfg(feature = "tracing")] + let _span = tracing::info_span!("wry::custom_protocol::call_handler").entered(); function(final_request, RequestAsyncResponder { responder }); } Err(_) => respond_with_404(), @@ -942,28 +952,39 @@ r#"Object.defineProperty(window, 'ipc', { } else { // Safety: objc runtime calls are unsafe unsafe { - let _: id = match callback { - Some(callback) => { - let handler = block::ConcreteBlock::new(|val: id, _err: id| { - let mut result = String::new(); + #[cfg(feature = "tracing")] + let span = Mutex::new(Some(tracing::debug_span!("wry::eval").entered())); - if val != nil { - let serializer = class!(NSJSONSerialization); - let json_ns_data: NSData = msg_send![serializer, dataWithJSONObject:val options:NS_JSON_WRITING_FRAGMENTS_ALLOWED error:nil]; - let json_string = NSString::from(json_ns_data); + // we need to check if the callback exists outside the handler otherwise it's a segfault + if let Some(callback) = &callback { + let handler = block::ConcreteBlock::new(move |val: id, _err: id| { + #[cfg(feature = "tracing")] + span.lock().unwrap().take(); - result = json_string.to_str().to_string(); - } + let mut result = String::new(); - callback(result) - }); + if val != nil { + let serializer = class!(NSJSONSerialization); + let json_ns_data: NSData = msg_send![serializer, dataWithJSONObject:val options:NS_JSON_WRITING_FRAGMENTS_ALLOWED error:nil]; + let json_string = NSString::from(json_ns_data); - msg_send![self.webview, evaluateJavaScript:NSString::new(js) completionHandler:handler] - } - None => { - msg_send![self.webview, evaluateJavaScript:NSString::new(js) completionHandler:null::<*const c_void>()] - } - }; + result = json_string.to_str().to_string(); + } + + callback(result); + }); + + let _: () = + msg_send![self.webview, evaluateJavaScript:NSString::new(js) completionHandler:handler]; + } else { + let handler = block::ConcreteBlock::new(move |_val: id, _err: id| { + #[cfg(feature = "tracing")] + span.lock().unwrap().take(); + }); + + let _: () = + msg_send![self.webview, evaluateJavaScript:NSString::new(js) completionHandler:handler]; + } } }