diff --git a/Cargo.lock b/Cargo.lock index e7c233e5..5c6eb26d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1926,16 +1926,6 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" -[[package]] -name = "log-panics" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f9dd8546191c1850ecf67d22f5ff00a935b890d0e84713159a55495cc2ac5f" -dependencies = [ - "backtrace", - "log", -] - [[package]] name = "lru" version = "0.12.4" @@ -2769,9 +2759,9 @@ version = "0.1.0" dependencies = [ "android-activity", "android_logger", + "backtrace", "jni", "log", - "log-panics", "ndk", "ndk-context", "ruffle_core", diff --git a/Cargo.toml b/Cargo.toml index b0f9c9e4..28d3df2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,10 +40,10 @@ ruffle_video_software = { git = "https://github.com/ruffle-rs/ruffle.git", branc ruffle_frontend_utils = { git = "https://github.com/ruffle-rs/ruffle.git", branch = "master" } log = "0.4.22" -log-panics = { version = "2.1.0", features = ["with-backtrace"]} # Redirect tracing to log tracing = {version = "0.1.40", features = ["log", "log-always"]} +backtrace = "0.3.74" url = "2.5.2" webbrowser = "1.0.1" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 800c4d63..4eb9de51 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,68 +1,73 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/rs/ruffle/PanicActivity.kt b/app/src/main/java/rs/ruffle/PanicActivity.kt new file mode 100644 index 00000000..66641c3c --- /dev/null +++ b/app/src/main/java/rs/ruffle/PanicActivity.kt @@ -0,0 +1,20 @@ +package rs.ruffle + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import rs.ruffle.ui.theme.RuffleTheme + +class PanicActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + setContent { + RuffleTheme { + PanicScreen(message = intent.getStringExtra("message") ?: "Unknown") + } + } + } +} diff --git a/app/src/main/java/rs/ruffle/PanicScreen.kt b/app/src/main/java/rs/ruffle/PanicScreen.kt new file mode 100644 index 00000000..66b948b5 --- /dev/null +++ b/app/src/main/java/rs/ruffle/PanicScreen.kt @@ -0,0 +1,77 @@ +package rs.ruffle + +import android.content.res.Configuration +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import rs.ruffle.ui.theme.RuffleTheme + +@Composable +fun PanicScreen(message: String) { + Scaffold { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .fillMaxSize(), + verticalArrangement = Arrangement.Center + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .wrapContentSize(align = Alignment.Center), + style = MaterialTheme.typography.headlineLarge, + text = "Ruffle Panicked :(" + ) + SelectionContainer { + Text( + modifier = Modifier + .wrapContentSize(align = Alignment.Center) + .padding(horizontal = 8.dp, vertical = 20.dp) + .verticalScroll(rememberScrollState()) + .horizontalScroll(rememberScrollState()), + text = message, + softWrap = false + ) + } + } + } +} + +@Preview(name = "Panic - Light", uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(name = "Panic - Dark", uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun PanicScreenPreview() { + RuffleTheme { + PanicScreen( + message = """Error: panicked at core/src/display_object/movie_clip.rs:477:9: +assertion `left == right` failed: Called replace_movie on a clip with LoaderInfo set + left: Some(LoaderInfoObject(LoaderInfoObject { ptr: 0x31b30a8 })) + right: None + at n.wbg.__wbg_new_796382978dfd4fb0 (https://unpkg.com/@ruffle-rs/ruffle/core.ruffle.90db0a0ab193ed0c601b.js:1:83857) + at ruffle_web.wasm.js_sys::Error::new::hfb561c222a4e70eb (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[12733]:0x98671a) + at ruffle_web.wasm.core::ops::function::FnOnce::call_once{{vtable.shim}}::h8a2a563fa204b611 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[9789]:0x9164aa) + at ruffle_web.wasm.std::panicking::rust_panic_with_hook::h33fe77d38d305ca3 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[6355]:0x8070ed) + at ruffle_web.wasm.core::panicking::panic_fmt::hde8b7aa66e2831e1 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[9511]:0x9071fd) + at ruffle_web.wasm.core::panicking::assert_failed_inner::hc95b7725cb4077cb (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[4402]:0x73cb5e) + at ruffle_web.wasm.ruffle_core::display_object::movie_clip::MovieClip::replace_with_movie::haf940b0718ed269c (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[2052]:0x50a035) + at ruffle_web.wasm.ruffle_core::loader::Loader::movie_loader::{{closure}}::h566c935379317178 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[1053]:0x2bc268) + at ruffle_web.wasm.::spawn_future::{{closure}}::h13f3540dbe40e875 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[1520]:0x419980) + at ruffle_web.wasm.wasm_bindgen_futures::queue::Queue::new::{{closure}}::hf37247571cf9bbf7 (wasm://wasm/ruffle_web.wasm-0321683a:wasm-function[3648]:0x6ba342)""" + ) + } +} diff --git a/app/src/main/java/rs/ruffle/PlayerActivity.kt b/app/src/main/java/rs/ruffle/PlayerActivity.kt index 5f223f5a..e19fd879 100644 --- a/app/src/main/java/rs/ruffle/PlayerActivity.kt +++ b/app/src/main/java/rs/ruffle/PlayerActivity.kt @@ -7,6 +7,7 @@ import android.net.Uri import android.os.Build import android.os.Build.VERSION_CODES import android.os.Bundle +import android.util.Log import android.view.Menu import android.view.MenuItem import android.view.MotionEvent @@ -230,6 +231,14 @@ class PlayerActivity : GameActivity() { } override fun onCreate(savedInstanceState: Bundle?) { + nativeInit { message -> + Log.e("ruffle", "Handling panic: $message") + startActivity( + Intent(this, PanicActivity::class.java).apply { + putExtra("message", message) + } + ) + } // When true, the app will fit inside any system UI windows. // When false, we render behind any system UI windows. WindowCompat.setDecorFitsSystemWindows(window, false) @@ -264,11 +273,10 @@ class PlayerActivity : GameActivity() { init { // load the native activity System.loadLibrary("ruffle_android") - nativeInit() } @JvmStatic - private external fun nativeInit() + private external fun nativeInit(crashCallback: CrashCallback) private fun gatherAllDescendantsOfType(v: View, t: Class<*>): List { val result: MutableList = ArrayList() @@ -282,4 +290,8 @@ class PlayerActivity : GameActivity() { return result } } + + fun interface CrashCallback { + fun onCrash(message: String) + } } diff --git a/src/java.rs b/src/java.rs index abbb650f..3bbde2b5 100644 --- a/src/java.rs +++ b/src/java.rs @@ -172,33 +172,31 @@ impl JavaInterface { } pub fn init(env: &mut JNIEnv, class: &JClass) { - JAVA_INTERFACE - .set(JavaInterface { - get_surface_width: env - .get_method_id(class, "getSurfaceWidth", "()I") - .expect("getSurfaceWidth must exist"), - get_surface_height: env - .get_method_id(class, "getSurfaceHeight", "()I") - .expect("getSurfaceHeight must exist"), - show_context_menu: env - .get_method_id(class, "showContextMenu", "([Ljava/lang/String;)V") - .expect("showContextMenu must exist"), - get_swf_bytes: env - .get_method_id(class, "getSwfBytes", "()[B") - .expect("getSwfBytes must exist"), - get_swf_uri: env - .get_method_id(class, "getSwfUri", "()Ljava/lang/String;") - .expect("getSwfUri must exist"), - get_trace_output: env - .get_method_id(class, "getTraceOutput", "()Ljava/lang/String;") - .expect("getTraceOutput must exist"), - get_loc_in_window: env - .get_method_id(class, "getLocInWindow", "()[I") - .expect("getLocInWindow must exist"), - get_android_data_storage_dir: env - .get_method_id(class, "getAndroidDataStorageDir", "()Ljava/lang/String;") - .expect("getAndroidDataStorageDir must exist"), - }) - .unwrap_or_else(|_| panic!("Init cannot be called more than once!")) + let _ = JAVA_INTERFACE.set(JavaInterface { + get_surface_width: env + .get_method_id(class, "getSurfaceWidth", "()I") + .expect("getSurfaceWidth must exist"), + get_surface_height: env + .get_method_id(class, "getSurfaceHeight", "()I") + .expect("getSurfaceHeight must exist"), + show_context_menu: env + .get_method_id(class, "showContextMenu", "([Ljava/lang/String;)V") + .expect("showContextMenu must exist"), + get_swf_bytes: env + .get_method_id(class, "getSwfBytes", "()[B") + .expect("getSwfBytes must exist"), + get_swf_uri: env + .get_method_id(class, "getSwfUri", "()Ljava/lang/String;") + .expect("getSwfUri must exist"), + get_trace_output: env + .get_method_id(class, "getTraceOutput", "()Ljava/lang/String;") + .expect("getTraceOutput must exist"), + get_loc_in_window: env + .get_method_id(class, "getLocInWindow", "()[I") + .expect("getLocInWindow must exist"), + get_android_data_storage_dir: env + .get_method_id(class, "getAndroidDataStorageDir", "()Ljava/lang/String;") + .expect("getAndroidDataStorageDir must exist"), + }); } } diff --git a/src/lib.rs b/src/lib.rs index c85c1aa5..6150db21 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,13 +18,16 @@ use std::sync::mpsc::Sender; use std::sync::{mpsc, MutexGuard}; use std::time::Duration; use std::{ + panic, sync::{Arc, Mutex}, + thread, time::Instant, }; use wgpu::rwh::{AndroidDisplayHandle, HasWindowHandle, RawDisplayHandle}; use android_activity::input::{InputEvent, KeyAction, MotionAction}; use android_activity::{AndroidApp, AndroidAppWaker, InputStatus, MainEvent, PollEvent}; +use backtrace::Backtrace; use jni::objects::JClass; use audio::AAudioAudioBackend; @@ -565,7 +568,69 @@ pub unsafe extern "C" fn Java_rs_ruffle_PlayerActivity_clearContextMenu( #[no_mangle] #[allow(clippy::missing_safety_doc)] -pub unsafe extern "C" fn Java_rs_ruffle_PlayerActivity_nativeInit(mut env: JNIEnv, class: JClass) { +pub unsafe extern "C" fn Java_rs_ruffle_PlayerActivity_nativeInit( + mut env: JNIEnv, + class: JClass, + crash_callback: JObject, +) { + let crash_callback = env.new_global_ref(crash_callback).unwrap(); + let jvm = env.get_java_vm().unwrap(); + + android_logger::init_once( + android_logger::Config::default() + .with_max_level(log::LevelFilter::Info) + .with_tag("ruffle") + .with_filter( + android_logger::FilterBuilder::new() + .parse("warn,ruffle=info") + .build(), + ), + ); + + panic::set_hook(Box::new(move |info| { + let backtrace = Backtrace::new(); + let thread = thread::current(); + let thread = thread.name().unwrap_or(""); + let message = match info.payload().downcast_ref::<&'static str>() { + Some(s) => *s, + None => match info.payload().downcast_ref::() { + Some(s) => &**s, + None => "Box", + }, + }; + + let full = match info.location() { + Some(location) => format!( + "thread '{}' panicked at '{}': {}:{}\n{:?}", + thread, + message, + location.file(), + location.line(), + backtrace + ), + None => format!( + "thread '{}' panicked at '{}'\n{:?}", + thread, message, backtrace + ), + }; + log::error!(target: "panic","{}", full); + + let mut env = jvm.attach_current_thread().unwrap(); + if env.exception_check().unwrap() { + // There's a pending exception, java will discover this on their own + } else { + let java_message = env.new_string(full).unwrap(); + let crash_callback = env.new_global_ref(&crash_callback).unwrap(); + env.call_method( + crash_callback, + "onCrash", + "(Ljava/lang/String;)V", + &[(&java_message).into()], + ) + .unwrap(); + } + })); + JavaInterface::init(&mut env, &class) } @@ -591,19 +656,6 @@ fn get_view_size() -> Result<(i32, i32), Box> { #[no_mangle] fn android_main(app: AndroidApp) { - android_logger::init_once( - android_logger::Config::default() - .with_max_level(log::LevelFilter::Info) - .with_tag("ruffle") - .with_filter( - android_logger::FilterBuilder::new() - .parse("warn,ruffle=info") - .build(), - ), - ); - - log_panics::init(); - log::info!("Starting android_main..."); run(app); }