Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Mono.Android] Marshal .NET stack trace to Throwable.getStackTrace() (#…
…8185) Context: #1198 Context: #1188 (comment) Context: #4877 Context: #4927 (comment) What happens with unhandled exceptions? throw new InvalidOperationException ("oops!"); This is a surprisingly complicated question: If this happens when a debugger is attached, the debugger will get a "first chance notification" at the `throw` site. If execution continues, odds are high that the app will abort if there is a JNI transition in the callstack. If no debugger is attached, then it depends on which thread threw the unhandled exception. If the thread which threw the unhandled exception is a .NET Thread: static void ThrowFromAnotherManagedThread() { var t = new System.Threading.Thread(() => { throw new new Java.Lang.Error ("from another thread?!"); }); t.Start (); t.Join (); } Then .NET will report the unhandled exception, *and* the app will restart: F mono-rt : [ERROR] FATAL UNHANDLED EXCEPTION: System.InvalidOperationException: oops! F mono-rt : at android_unhandled_exception.MainActivity.<>c.<ThrowFromAnotherManagedThread>b__1_0() F mono-rt : at System.Threading.Thread.StartCallback() # app restarts If the thread which threw the unhandled exception is a *Java* thread, which could be the UI thread (e.g. thrown from an `Activity.OnCreate()` override) or via a `Java.Lang.Thread` instance: static void ThrowFromAnotherJavaThread() { var t = new Java.Lang.Thread(() => { throw new InvalidOperationException ("oops!"); }); t.Start (); t.Join (); } Then .NET will report the unhandled exception, *and* the app will *not* restart (which differs from using .NET threads): E AndroidRuntime: Process: com.companyname.android_unhandled_exception, PID: 5436 E AndroidRuntime: android.runtime.JavaProxyThrowable: System.InvalidOperationException: oops! E AndroidRuntime: at android_unhandled_exception.MainActivity.<>c.<ThrowFromAnotherJavaThread>b__2_0() E AndroidRuntime: at Java.Lang.Thread.RunnableImplementor.Run() E AndroidRuntime: at Java.Lang.IRunnableInvoker.n_Run(IntPtr , IntPtr ) E AndroidRuntime: at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PP_V(_JniMarshal_PP_V , IntPtr , IntPtr ) E AndroidRuntime: at mono.java.lang.RunnableImplementor.n_run(Native Method) E AndroidRuntime: at mono.java.lang.RunnableImplementor.run(RunnableImplementor.java:31) E AndroidRuntime: at java.lang.Thread.run(Thread.java:1012) I MonoDroid: Android.Runtime.JavaProxyThrowable: Exception_WasThrown, Android.Runtime.JavaProxyThrowable I MonoDroid: I MonoDroid: --- End of managed Android.Runtime.JavaProxyThrowable stack trace --- I MonoDroid: android.runtime.JavaProxyThrowable: System.InvalidOperationException: oops! I MonoDroid: at android_unhandled_exception.MainActivity.<>c.<ThrowFromAnotherJavaThread>b__2_0() I MonoDroid: at Java.Lang.Thread.RunnableImplementor.Run() I MonoDroid: at Java.Lang.IRunnableInvoker.n_Run(IntPtr , IntPtr ) I MonoDroid: at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PP_V(_JniMarshal_PP_V , IntPtr , IntPtr ) I MonoDroid: at mono.java.lang.RunnableImplementor.n_run(Native Method) I MonoDroid: at mono.java.lang.RunnableImplementor.run(RunnableImplementor.java:31) I MonoDroid: at java.lang.Thread.run(Thread.java:1012) I MonoDroid: I MonoDroid: --- End of managed Android.Runtime.JavaProxyThrowable stack trace --- I MonoDroid: android.runtime.JavaProxyThrowable: System.InvalidOperationException: oops! I MonoDroid: at android_unhandled_exception.MainActivity.<>c.<ThrowFromAnotherJavaThread>b__2_0() I MonoDroid: at Java.Lang.Thread.RunnableImplementor.Run() I MonoDroid: at Java.Lang.IRunnableInvoker.n_Run(IntPtr , IntPtr ) I MonoDroid: at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PP_V(_JniMarshal_PP_V , IntPtr , IntPtr ) I MonoDroid: at mono.java.lang.RunnableImplementor.n_run(Native Method) I MonoDroid: at mono.java.lang.RunnableImplementor.run(RunnableImplementor.java:31) I MonoDroid: at java.lang.Thread.run(Thread.java This "works", until we enter the world of crash logging for later diagnosis and fixing. The problem with our historical approach is that we would "stuff" the .NET stack trace into the "message" of the Java-side `Throwable` instance, and the "message" may not be transmitted as part of the crash logging! (This is noticeable by the different indentation levels for the `at …` lines in the crash output. Three space indents are from the `Throwable.getMessage()` output, while four space indents are from the Java-side stack trace.) We *think* that we can improve this by replacing the Java-side stack trace with a "merged" stack trace which includes both the Java-side and .NET-side stack traces. This does nothing for unhandled exceptions on .NET threads, but does alter the output from Java threads: E AndroidRuntime: FATAL EXCEPTION: Thread-3 E AndroidRuntime: Process: com.companyname.android_unhandled_exception, PID: 12321 E AndroidRuntime: android.runtime.JavaProxyThrowable: [System.InvalidOperationException]: oops! E AndroidRuntime: at android_unhandled_exception.MainActivity+<>c.<ThrowFromAnotherJavaThread>b__2_0(Unknown Source:0) E AndroidRuntime: at Java.Lang.Thread+RunnableImplementor.Run(Unknown Source:0) E AndroidRuntime: at Java.Lang.IRunnableInvoker.n_Run(Unknown Source:0) E AndroidRuntime: at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PP_V(Unknown Source:0) E AndroidRuntime: at mono.java.lang.RunnableImplementor.n_run(Native Method) E AndroidRuntime: at mono.java.lang.RunnableImplementor.run(RunnableImplementor.java:31) E AndroidRuntime: at java.lang.Thread.run(Thread.java:1012) I MonoDroid: UNHANDLED EXCEPTION: I MonoDroid: Android.Runtime.JavaProxyThrowable: Exception_WasThrown, Android.Runtime.JavaProxyThrowable I MonoDroid: I MonoDroid: --- End of managed Android.Runtime.JavaProxyThrowable stack trace --- I MonoDroid: android.runtime.JavaProxyThrowable: [System.InvalidOperationException]: oops! I MonoDroid: at android_unhandled_exception.MainActivity+<>c.<ThrowFromAnotherJavaThread>b__2_0(Unknown Source:0) I MonoDroid: at Java.Lang.Thread+RunnableImplementor.Run(Unknown Source:0) I MonoDroid: at Java.Lang.IRunnableInvoker.n_Run(Unknown Source:0) I MonoDroid: at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PP_V(Unknown Source:0) I MonoDroid: at mono.java.lang.RunnableImplementor.n_run(Native Method) I MonoDroid: at mono.java.lang.RunnableImplementor.run(RunnableImplementor.java:31) I MonoDroid: at java.lang.Thread.run(Thread.java:1012) I MonoDroid: I MonoDroid: --- End of managed Android.Runtime.JavaProxyThrowable stack trace --- I MonoDroid: android.runtime.JavaProxyThrowable: [System.InvalidOperationException]: oops! I MonoDroid: at android_unhandled_exception.MainActivity+<>c.<ThrowFromAnotherJavaThread>b__2_0(Unknown Source:0) I MonoDroid: at Java.Lang.Thread+RunnableImplementor.Run(Unknown Source:0) I MonoDroid: at Java.Lang.IRunnableInvoker.n_Run(Unknown Source:0) I MonoDroid: at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PP_V(Unknown Source:0) I MonoDroid: at mono.java.lang.RunnableImplementor.n_run(Native Method) I MonoDroid: at mono.java.lang.RunnableImplementor.run(RunnableImplementor.java:31) I MonoDroid: at java.lang.Thread.run(Thread.java:1012) Note how `at …` is always a four-space indent and always lines up. *Hopefully* this means that crash loggers can provide more useful information. TODO: * Create an "end-to-end" test which uses an actual crash logger (which one?) in order to better understand what the "end user" experience is. * The "merged" stack trace always places the managed stack trace above the Java-side stack trace. This means things will look "weird"/"wrong" if you have an *intermixed* stack trace, e.g. (Java code calls .NET code which calls Java code)+ which eventually throws from .NET.
- Loading branch information