Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve JavaProxyThrowable by translating managed stack trace #8185

Merged
merged 25 commits into from
Aug 23, 2023

Conversation

grendello
Copy link
Contributor

Fixes: #1198

Given an instance of System.Exception, we can use the
System.Diagnostics.StackTrace class to iterate over the list of
managed stack frames, translating each of them to equivalent
Java.Lang.StackTraceElement and afterwards passing the entire
collection to Java.Lang.Throwable.SetStackTrace.

Fixes: dotnet#1198

Given an instance of `System.Exception`, we can use the
`System.Diagnostics.StackTrace` class to iterate over the list of
managed stack frames, translating each of them to equivalent
`Java.Lang.StackTraceElement` and afterwards passing the entire
collection to `Java.Lang.Throwable.SetStackTrace`.
@grendello grendello requested a review from jonpryor as a code owner July 12, 2023 10:10
@AmrAlSayed0
Copy link

I think the first thing that's going to break is the public API surface. There is no new JavaProxyThrowable(ex) constructor anymore. It was public before so there is a chance someone was using it.

@grendello
Copy link
Contributor Author

I think the first thing that's going to break is the public API surface. There is no new JavaProxyThrowable(ex) constructor anymore. It was public before so there is a chance someone was using it.

It's fine, the class is internal to Mono.Android.dll


namespace Android.Runtime {

class JavaProxyThrowable : Java.Lang.Error {

public readonly Exception InnerException;

public JavaProxyThrowable (Exception innerException)
: base (GetDetailMessage (innerException))
protected JavaProxyThrowable (string message, Exception innerException)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type should probably be sealed -- nothing subclasses it, and I can't think of anything to subclass it ever -- and this constructor could thus be private.

@jonpryor
Copy link
Member

This should also update tests/Mono.Android-Tests/System/ExceptionTest.cs to add a test to assert the "merged" stack frame contents.

NOTE: test, umm, untested! Unable to build the project locally
@grendello
Copy link
Contributor Author

This should also update tests/Mono.Android-Tests/System/ExceptionTest.cs to add a test to assert the "merged" stack frame contents.

Pushed the additional code to the test, but I wasn't able to check whether it even compiles locally. Can't build the test solution or project here :(

@grendello
Copy link
Contributor Author

OK, found the right project to build, at least it compiles. We'll see if it runs.

return proxy;
}

void TranslateStackTrace ()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with this method is that it doesn't "merge", it "replaces". #1188 should have provided examples for desired behavior.

Consider:

var e = new Java.Lang.Error ("here!");
e.PrintStackTrace(Java.Lang.JavaSystem.Out);

This produces the output:

java.lang.Error: here!
   at crc641c2f9234c37aaa3c.MainActivity.n_onCreate(Native Method)
   at crc641c2f9234c37aaa3c.MainActivity.onCreate(MainActivity.java:30)
   at android.app.Activity.performCreate(Activity.java:8341)
   at android.app.Activity.performCreate(Activity.java:8320)
   at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1417)
   at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3622)
   at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3778)
   at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
   at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138)
   at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2303)
   at android.os.Handler.dispatchMessage(Handler.java:106)
   at android.os.Looper.loopOnce(Looper.java:201)
   at android.os.Looper.loop(Looper.java:288)
   at android.app.ActivityThread.main(ActivityThread.java:7884)
   at java.lang.reflect.Method.invoke(Native Method)
   at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

If the assumption in #1188 is correct, and we want printStackTrace() to be "more useful", replacing all of the above is not "more useful". What we'd want is to instead produce:

java.lang.Error: here!
   at android_cwd.MainActivity.OnCreate(MainActivity.cs:11)
   at crc641c2f9234c37aaa3c.MainActivity.n_onCreate(Native Method)
   at crc641c2f9234c37aaa3c.MainActivity.onCreate(MainActivity.java:30)
…

As far as I can tell, TranslateStackTrace() does not do any such "merging". It would thus instead produce:

   at android_cwd.MainActivity.OnCreate(MainActivity.cs:11)

as the complete stack trace, which I don't believe would be useful.

(Now, this PR/#1188 is about JavaProxyThrowable, not for new Error().PrintStackTrace(), but PrintStackTrace() is the main point.)

grendello added 11 commits July 14, 2023 19:51
* main:
  [ci] XA.PublishAllLogs publishes all build logs to build artifacts (dotnet#8189)
  Bump to xamarin/Java.Interop/main@151b03e (dotnet#8188)
  [Mono.Android] Use PublicApiAnalyzers to ensure we do not break API. (dotnet#8171)
  [Xamarin.Android.Build.Tasks] per-RID assemblies & typemaps (dotnet#8164)
  [AndroidDependenciesTests] Test both manifest types (dotnet#8186)
  [AndroidDependenciesTests] Use platform-tools 34.0.4 (dotnet#8184)
* main:
  LLVM IR code generator refactoring and updates (dotnet#8140)
* main:
  [ci] Include build components in SBOM (dotnet#8174)
* main:
  [Xamarin.Android.Build.Tasks] fix `.aar` files flowing from project references (dotnet#8193)
* main:
  $(AndroidPackVersionSuffix)=rc.1; net8 is 34.0.0-rc.1 (dotnet#8204)
  Bump to dotnet/installer@ca467d68c8 8.0.100-preview.7.23364.32 (dotnet#8176)
  Clean up DotNetIgnore Unit Tests (dotnet#8163)
  [Xamarin.Android.Build.Tasks] fix duplicate `.aar` files (dotnet#8196)
  [Documentation] Appease PoliCheck Rule: 79604 (dotnet#8197)
static string GetDetailMessage (Exception innerException)
public static JavaProxyThrowable Create (Exception? innerException, bool appendJavaStackTrace = false)
{
if (innerException == null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have Exception? innerException if you're gonna throw ArgumentNullException here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a typo

innerException passed to JavaProxyThrowable.Create shouldn't be allowed to be null
@jonpryor
Copy link
Member

The real, fundamental, question/concern/whatever: what's the end-to-end scenario, and what's it look like, and is this an improvement?

Which requires resolving a great number of unknowns: what "unhandled exception crash logger" is being used? What information does it capture? How is it presented? (#1188 mentions HockeyApp, which may have been shut down?)

The fundamental assumption in #1198 is that whatever the crash reporter is, it may (was?) dropping Throwable.getMessage() on the floor, but would preserve Throwable.getStackTrace().

Is that assumption even correct?

If it is correct, what do things look like now?

Thus:

diff --git a/src/java-runtime/java/mono/android/Runtime.java b/src/java-runtime/java/mono/android/Runtime.java
index 01e62b239..a018d5708 100644
--- a/src/java-runtime/java/mono/android/Runtime.java
+++ b/src/java-runtime/java/mono/android/Runtime.java
@@ -2,6 +2,7 @@ package mono.android;
 
 import java.lang.Thread;
 import java.lang.Throwable;
+import android.util.Log;
 
 public class Runtime {
 	static java.lang.Class java_lang_Class = java.lang.Class.class;;
@@ -49,6 +50,12 @@ final class XamarinUncaughtExceptionHandler implements Thread.UncaughtExceptionH
 	public final void uncaughtException (Thread t, Throwable e)
 	{
 		Runtime.propagateUncaughtException (t, e);
+		StackTraceElement[] stack = e.getStackTrace();
+		Log.w("*jonp*", "# jonp: e.Message=" + e.getMessage());
+		for (int i = 0; i < stack.length; ++i) {
+			StackTraceElement s = stack [i];
+			Log.w ("*jonp*", "# jonp: stack[" + i + "]=" + s.getClassName() + "." + s.getMethodName() + "(" + s.getFileName() + ":" + s.getLineNumber() + ")");
+		}
 
 		if (defaultHandler != null)
 			defaultHandler.uncaughtException (t, e);

This updates XamarinUncaughtExceptionHandler to print out the various stack frames of the uncaught exception, which hopefully would match whatever our hypothetical crash reporter would see.

Next, the app! dotnet new andrdoid, and have MainActivity.OnCreate() throw an InvalidOperationException.

The result with 57eedfb (not #8185):

W *jonp*  : # jonp: e.Message=System.InvalidOperationException: wee!
W *jonp*  :    at android_unhandled_exception.MainActivity.OnCreate(Bundle savedInstanceState)
W *jonp*  :    at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState)
W *jonp*  :    at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V(_JniMarshal_PPL_V callback, IntPtr jnienv, IntPtr klazz, IntPtr p0)
W *jonp*  : # jonp: stack[0]=crc64ef325ad1a3b68b57.MainActivity.n_onCreate(MainActivity.java:-2)
W *jonp*  : # jonp: stack[1]=crc64ef325ad1a3b68b57.MainActivity.onCreate(MainActivity.java:30)
W *jonp*  : # jonp: stack[2]=android.app.Activity.performCreate(Activity.java:8342)
W *jonp*  : # jonp: stack[3]=android.app.Activity.performCreate(Activity.java:8321)
W *jonp*  : # jonp: stack[4]=android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1417)
W *jonp*  : # jonp: stack[5]=android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3625)
W *jonp*  : # jonp: stack[6]=android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3781)
W *jonp*  : # jonp: stack[7]=android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
W *jonp*  : # jonp: stack[8]=android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138)
W *jonp*  : # jonp: stack[9]=android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
W *jonp*  : # jonp: stack[10]=android.app.ActivityThread$H.handleMessage(ActivityThread.java:2306)
W *jonp*  : # jonp: stack[11]=android.os.Handler.dispatchMessage(Handler.java:106)
W *jonp*  : # jonp: stack[12]=android.os.Looper.loopOnce(Looper.java:201)
W *jonp*  : # jonp: stack[13]=android.os.Looper.loop(Looper.java:288)
W *jonp*  : # jonp: stack[14]=android.app.ActivityThread.main(ActivityThread.java:7918)
W *jonp*  : # jonp: stack[15]=java.lang.reflect.Method.invoke(Method.java:-2)
W *jonp*  : # jonp: stack[16]=com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
W *jonp*  : # jonp: stack[17]=com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

from which we see that both Throwable.getMessage() and Throwable.getStackTrace() are required to make any sense out of things. If Throwable.getMessage() is lost or truncated, things suck. This suggests that the assumption of #1198 may in fact be correct: "marshal" the C# stack trace into Throwable.setStackTrace() may allow the crash reporter to more reasonably preserve both sides with higher fidelity, and reduce the reliance on Throwable.getMessage() preservation.

That background thinking in mind, how does this PR #8185 change things?

I have no idea, as there are no "end-to-end" tests to speak of. It would be useful to have an on-device test which reads adb logcat from after the exception to see/compare what the actual adb logcat is.

Additionally, the above unhandled exception output is the "easy" case: Java calls C# code, which throws an exception. What happens for a "mixed" case: Java calls C# code which calls into Java which calls into C# which throws an exception? I suspect that in such a case the Java callstack will be "weird and useless" -- you'll have a crc64….Type.n_whatever() method calling into Java, with no knowledge of what C# methods were in between those invocations -- and that the even more important Throwable.getMessage() output will be longer, more useful, and also more at risk of becoming truncated and thus useless.

@jonpryor
Copy link
Member

What do I want?

  1. An "End-to-end" test that at minimum captures & compares adb logcat output for the unhandled exception.
  2. (1) involving a Java > C# > Java > C# (throws) callstack
  3. "Some way" of knowing/verifying the contents of Throwable.getStackTrace() for the unhandled exception.

"Even better" would be a complete end-to-end case, of using Crashlytics or something, and seeing what is actually reported when the app has an unhandled exception for a Java > C# > Java > C# stack transition.

@jonpryor
Copy link
Member

Harking back to my previous comment, what does this PR do when using the Runtime.java patch?

One checkout, build, and test-run later, and adb logcat prints:

W *jonp*  : # jonp: e.Message=[System.Func`1[System.Type]]: wee!
W *jonp*  : # jonp: stack[0]=android_unhandled_exception.MainActivity.OnCreate(null:0)
W *jonp*  : # jonp: stack[1]=Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(null:0)
W *jonp*  : # jonp: stack[2]=Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V(null:0)

I do not consider this an improvement: we lose the entire Java-side callstack!

(Aside: [System.Func`1[System.Type]]?! Where did that come from? We're also losing the InvalidOperationException bit!)

Additionally, the "UNHANDLED EXCEPTION" output is likewise less useful (as far as I'm concerned), and comes from C# not Java:

This PR:

I MonoDroid: UNHANDLED EXCEPTION:
I MonoDroid: Android.Runtime.JavaProxyThrowable: Exception of type 'Android.Runtime.JavaProxyThrowable' was thrown.
I MonoDroid: 
I MonoDroid:   --- End of managed Android.Runtime.JavaProxyThrowable stack trace ---
I MonoDroid: android.runtime.JavaProxyThrowable: [System.Func`1[System.Type]]: wee!
I MonoDroid:     at android_unhandled_exception.MainActivity.OnCreate(Unknown Source:0)
I MonoDroid:     at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(Unknown Source:0)
I MonoDroid:     at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V(Unknown Source:0)
I MonoDroid: 
I MonoDroid:   --- End of managed Android.Runtime.JavaProxyThrowable stack trace ---
I MonoDroid: android.runtime.JavaProxyThrowable: [System.Func`1[System.Type]]: wee!
I MonoDroid:     at android_unhandled_exception.MainActivity.OnCreate(Unknown Source:0)
I MonoDroid:     at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(Unknown Source:0)
I MonoDroid:     at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V(Unknown Source:0)

(Again, note complete lack of Java-side information.)

57eedfb with Runtime.java changes:

I MonoDroid: UNHANDLED EXCEPTION:
I MonoDroid: Android.Runtime.JavaProxyThrowable: Exception of type 'Android.Runtime.JavaProxyThrowable' was thrown.
I MonoDroid: 
I MonoDroid:   --- End of managed Android.Runtime.JavaProxyThrowable stack trace ---
I MonoDroid: android.runtime.JavaProxyThrowable: System.InvalidOperationException: wee!
I MonoDroid:    at android_unhandled_exception.MainActivity.OnCreate(Bundle savedInstanceState)
I MonoDroid:    at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState)
I MonoDroid:    at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V(_JniMarshal_PPL_V callback, IntPtr jnienv, IntPtr klazz, IntPtr p0)
I MonoDroid:     at crc64ef325ad1a3b68b57.MainActivity.n_onCreate(Native Method)
I MonoDroid:     at crc64ef325ad1a3b68b57.MainActivity.onCreate(MainActivity.java:30)
I MonoDroid:     at android.app.Activity.performCreate(Activity.java:8342)
I MonoDroid:     at android.app.Activity.performCreate(Activity.java:8321)
I MonoDroid:     at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1417)
I MonoDroid:     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3625)
I MonoDroid:     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3781)
I MonoDroid:     at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
I MonoDroid:     at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138)
I MonoDroid:     at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
I MonoDroid:     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2306)
I MonoDroid:     at android.os.Handler.dispatchMessage(Handler.java:106)
I MonoDroid:     at android.os.Looper.loopOnce(Looper.java:201)
I MonoDroid:     at android.os.Looper.loop(Looper.java:288)
I MonoDroid:     at android.app.ActivityThread.main(ActivityThread.java:7918)
I MonoDroid:     at java.lang.reflect.Method.invoke(Native Method)
I MonoDroid:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
I MonoDroid:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
I MonoDroid: 
I MonoDroid:   --- End of managed Android.Runtime.JavaProxyThrowable stack trace ---
I MonoDroid: android.runtime.JavaProxyThrowable: System.InvalidOperationException: wee!
I MonoDroid:    at android_unhandled_exception.MainActivity.OnCreate(Bundle savedInstanceState)
I MonoDroid:    at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState)
I MonoDroid:    at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V(_JniMarshal_PPL_V callback, IntPtr jnienv, IntPtr klazz, IntPtr p0)
I MonoDroid:     at crc64ef325ad1a3b68b57.MainActivity.n_onCreate(Native Method)
I MonoDroid:     at crc64ef325ad1a3b68b57.MainActivity.onCreate(MainActivity.java:30)
I MonoDroid:     at android.app.Activity.performCreate(Activity.java:8342)
I MonoDroid:     at android.app.Activity.performCreate(Activity.java:8321)
I MonoDroid:     at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1417)
I MonoDroid:     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3625)
I MonoDroid:     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3781)
I MonoDroid:     at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
I MonoDroid:     at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138)
I MonoDroid:     at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
I MonoDroid:     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2306)
I MonoDroid:     at android.os.Handler.dispatchMessage(Handler.java:106)
I MonoDroid:     at android.os.Looper.loopOnce(Looper.java:201)
I MonoDroid:     at android.os.Looper.loop(Looper.java:288)
I MonoDroid:     at android.app.ActivityThread.main(ActivityThread.java:7918)
I MonoDroid:     at java.lang.reflect.Method.invoke(Native Method)
I MonoDroid:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
I MonoDroid:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

Which one of these would you rather have when diagnosing a crash? For me, the latter, hands-down.

I shudder to think of what the Visual Studio Unhandled Exception dialog would show if #8185 is present.

@jonpryor
Copy link
Member

The FATAL EXCEPTION message is similar to the UNHANDLED EXCEPTION message.

With #8185:

E AndroidRuntime: FATAL EXCEPTION: main
E AndroidRuntime: Process: com.companyname.android_unhandled_exception, PID: 414
E AndroidRuntime: android.runtime.JavaProxyThrowable: [System.Func`1[System.Type]]: wee!
E AndroidRuntime:        at android_unhandled_exception.MainActivity.OnCreate(Unknown Source:0)
E AndroidRuntime:        at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(Unknown Source:0)
E AndroidRuntime:        at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V(Unknown Source:0)

Current main:

E AndroidRuntime: FATAL EXCEPTION: main
E AndroidRuntime: Process: com.companyname.android_unhandled_exception, PID: 3277
E AndroidRuntime: android.runtime.JavaProxyThrowable: System.InvalidOperationException: wee!
E AndroidRuntime:    at android_unhandled_exception.MainActivity.OnCreate(Bundle savedInstanceState)
E AndroidRuntime:    at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(IntPtr jnienv, IntPtr native__this, IntPtr native_savedInstanceState)
E AndroidRuntime:    at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V(_JniMarshal_PPL_V callback, IntPtr jnienv, IntPtr klazz, IntPtr p0)
E AndroidRuntime:        at crc64ef325ad1a3b68b57.MainActivity.n_onCreate(Native Method)
E AndroidRuntime:        at crc64ef325ad1a3b68b57.MainActivity.onCreate(MainActivity.java:30)
E AndroidRuntime:        at android.app.Activity.performCreate(Activity.java:8342)
E AndroidRuntime:        at android.app.Activity.performCreate(Activity.java:8321)
E AndroidRuntime:        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1417)
E AndroidRuntime:        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3625)
E AndroidRuntime:        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3781)
E AndroidRuntime:        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
E AndroidRuntime:        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138)
E AndroidRuntime:        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
E AndroidRuntime:        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2306)
E AndroidRuntime:        at android.os.Handler.dispatchMessage(Handler.java:106)
E AndroidRuntime:        at android.os.Looper.loopOnce(Looper.java:201)
E AndroidRuntime:        at android.os.Looper.loop(Looper.java:288)
E AndroidRuntime:        at android.app.ActivityThread.main(ActivityThread.java:7918)
E AndroidRuntime:        at java.lang.reflect.Method.invoke(Native Method)
E AndroidRuntime:        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
E AndroidRuntime:        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

* main:
  [ci] Remove .NET branches from classic release trigger (dotnet#8218)
  Bump to dotnet/installer@f8bab721ae 8.0.100-rc.1.23373.1 (dotnet#8202)
  [Mono.Android] Fix Context.RegisterReceiver() enumification (dotnet#7735)
  [ci] Add MAUI integration job (dotnet#8200)
  [vs-workload] Set EnableSideBySideManifests=true (dotnet#8179)
…ndroid into throwable-stacktrace

* 'throwable-stacktrace' of github.com:grendello/xamarin-android:
  Update JavaProxyThrowable.cs
@jonpryor
Copy link
Member

With the latest changes in 603f1e4:

Throwable.getStackTrace() output via my patch:

W *jonp*  : # jonp: e.Message=[System.Func`1[System.Type]]: wee!
W *jonp*  : # jonp: stack[0]=android_unhandled_exception.MainActivity.OnCreate(null:0)
W *jonp*  : # jonp: stack[1]=Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(null:0)
W *jonp*  : # jonp: stack[2]=Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V(null:0)
W *jonp*  : # jonp: stack[3]=crc64ef325ad1a3b68b57.MainActivity.n_onCreate(MainActivity.java:-2)
W *jonp*  : # jonp: stack[4]=crc64ef325ad1a3b68b57.MainActivity.onCreate(MainActivity.java:30)
W *jonp*  : # jonp: stack[5]=android.app.Activity.performCreate(Activity.java:8342)
W *jonp*  : # jonp: stack[6]=android.app.Activity.performCreate(Activity.java:8321)
W *jonp*  : # jonp: stack[7]=android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1417)
W *jonp*  : # jonp: stack[8]=android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3625)
W *jonp*  : # jonp: stack[9]=android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3781)
W *jonp*  : # jonp: stack[10]=android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
W *jonp*  : # jonp: stack[11]=android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138)
W *jonp*  : # jonp: stack[12]=android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
W *jonp*  : # jonp: stack[13]=android.app.ActivityThread$H.handleMessage(ActivityThread.java:2306)
W *jonp*  : # jonp: stack[14]=android.os.Handler.dispatchMessage(Handler.java:106)
W *jonp*  : # jonp: stack[15]=android.os.Looper.loopOnce(Looper.java:201)
W *jonp*  : # jonp: stack[16]=android.os.Looper.loop(Looper.java:288)
W *jonp*  : # jonp: stack[17]=android.app.ActivityThread.main(ActivityThread.java:7918)
W *jonp*  : # jonp: stack[18]=java.lang.reflect.Method.invoke(Method.java:-2)
W *jonp*  : # jonp: stack[19]=com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
W *jonp*  : # jonp: stack[20]=com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

UNHANDLED EXCEPTION output:

I MonoDroid: UNHANDLED EXCEPTION:
I MonoDroid: Android.Runtime.JavaProxyThrowable: Exception of type 'Android.Runtime.JavaProxyThrowable' was thrown.
I MonoDroid: 
I MonoDroid:   --- End of managed Android.Runtime.JavaProxyThrowable stack trace ---
I MonoDroid: android.runtime.JavaProxyThrowable: [System.Func`1[System.Type]]: wee!
I MonoDroid:     at android_unhandled_exception.MainActivity.OnCreate(Unknown Source:0)
I MonoDroid:     at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(Unknown Source:0)
I MonoDroid:     at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V(Unknown Source:0)
I MonoDroid:     at crc64ef325ad1a3b68b57.MainActivity.n_onCreate(Native Method)
I MonoDroid:     at crc64ef325ad1a3b68b57.MainActivity.onCreate(MainActivity.java:30)
I MonoDroid:     at android.app.Activity.performCreate(Activity.java:8342)
I MonoDroid:     at android.app.Activity.performCreate(Activity.java:8321)
I MonoDroid:     at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1417)
I MonoDroid:     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3625)
I MonoDroid:     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3781)
I MonoDroid:     at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
I MonoDroid:     at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138)
I MonoDroid:     at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
I MonoDroid:     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2306)
I MonoDroid:     at android.os.Handler.dispatchMessage(Handler.java:106)
I MonoDroid:     at android.os.Looper.loopOnce(Looper.java:201)
I MonoDroid:     at android.os.Looper.loop(Looper.java:288)
I MonoDroid:     at android.app.ActivityThread.main(ActivityThread.java:7918)
I MonoDroid:     at java.lang.reflect.Method.invoke(Native Method)
I MonoDroid:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
I MonoDroid:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)
I MonoDroid: 
I MonoDroid:   --- End of managed Android.Runtime.JavaProxyThrowable stack trace ---
I MonoDroid: android.runtime.JavaProxyThrowable: [System.Func`1[System.Type]]: wee!
I MonoDroid:     at android_unhandled_exception.MainActivity.OnCreate(Unknown Source:0)
I MonoDroid:     at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(Unknown Source:0)
I MonoDroid:     at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V(Unknown Source:0)
I MonoDroid:     at crc64ef325ad1a3b68b57.MainActivity.n_onCreate(Native Method)
I MonoDroid:     at crc64ef325ad1a3b68b57.MainActivity.onCreate(MainActivity.java:30)
I MonoDroid:     at android.app.Activity.performCreate(Activity.java:8342)
I MonoDroid:     at android.app.Activity.performCreate(Activity.java:8321)
I MonoDroid:     at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1417)
I MonoDroid:     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3625)
I MonoDroid:     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3781)
I MonoDroid:     at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
I MonoDroid:     at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138)
I MonoDroid:     at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
I MonoDroid:     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2306)
I MonoDroid:     at android.os.Handler.dispatchMessage(Handler.java:106)
I MonoDroid:     at android.os.Looper.loopOnce(Looper.java:201)
I MonoDroid:     at android.os.Looper.loop(Looper.java:288)
I MonoDroid:     at android.app.ActivityThread.main(ActivityThread.java:7918)
I MonoDroid:     at java.lang.reflect.Method.invoke(Native Method)
I MonoDroid:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
I MonoDroid:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

FATAL EXCEPTION output:

E AndroidRuntime: FATAL EXCEPTION: main
E AndroidRuntime: Process: com.companyname.android_unhandled_exception, PID: 31011
E AndroidRuntime: android.runtime.JavaProxyThrowable: [System.Func`1[System.Type]]: wee!
E AndroidRuntime:        at android_unhandled_exception.MainActivity.OnCreate(Unknown Source:0)
E AndroidRuntime:        at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(Unknown Source:0)
E AndroidRuntime:        at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V(Unknown Source:0)
E AndroidRuntime:        at crc64ef325ad1a3b68b57.MainActivity.n_onCreate(Native Method)
E AndroidRuntime:        at crc64ef325ad1a3b68b57.MainActivity.onCreate(MainActivity.java:30)
E AndroidRuntime:        at android.app.Activity.performCreate(Activity.java:8342)
E AndroidRuntime:        at android.app.Activity.performCreate(Activity.java:8321)
E AndroidRuntime:        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1417)
E AndroidRuntime:        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3625)
E AndroidRuntime:        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3781)
E AndroidRuntime:        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:101)
E AndroidRuntime:        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:138)
E AndroidRuntime:        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
E AndroidRuntime:        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2306)
E AndroidRuntime:        at android.os.Handler.dispatchMessage(Handler.java:106)
E AndroidRuntime:        at android.os.Looper.loopOnce(Looper.java:201)
E AndroidRuntime:        at android.os.Looper.loop(Looper.java:288)
E AndroidRuntime:        at android.app.ActivityThread.main(ActivityThread.java:7918)
E AndroidRuntime:        at java.lang.reflect.Method.invoke(Native Method)
E AndroidRuntime:        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
E AndroidRuntime:        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:936)

I think this is an improvement.

One thing does confuse me, though: Why is InvalidOperationException lost and replaced with [System.Func`1[System.Type]]?

}

// We prepend managed exception type to message since Java will see `JavaProxyThrowable` instead.
var proxy = new JavaProxyThrowable ($"[{innerException.GetType}]: {innerException.Message}", innerException);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where [System.Func`1[System.Type]] is coming from!

Which still doesn't make sense, as that would mean that innerException.GetType() is Func<Type>, but innerException is an Exception, so… how is that possible? I remain flummoxed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh! It's because () is missing! This should be innerException.GetType() (with parens), not innerException.GetType! I'm kinda surprised that innerException.GetType even compiled; apparently the compiler was just implicitly converting it to a Func<Type>, which kinda makes sense?

declaringClass: managedMethod?.DeclaringType?.FullName,
methodName: managedMethod?.Name,
fileName: managedFrame?.GetFileName (),
lineNumber: managedFrame?.GetFileLineNumber () ?? 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be ?? -1, as negative numbers appear to be the "don't have one" behavior. See e.g.

W *jonp*  : # jonp: stack[3]=crc64ef325ad1a3b68b57.MainActivity.n_onCreate(MainActivity.java:-2)

No idea where -2 came from; I assume it's because the file has no line number info? (Which also makes no sense, as the declaration for n_onCreate() does have a line number…)

* main:
  [build] Use .NET preview to create nupkgs (dotnet#8227)
  Bump to xamarin/xamarin-android-binutils/L_16.0.6-6.0.0@b38b5bab (dotnet#8220)
  [ci] Fix MAUI integration test job (dotnet#8221)
@jonpryor
Copy link
Member

jonpryor commented Aug 18, 2023

WIP commit message:

Context: https://github.com/xamarin/xamarin-android/issues/1198
Context: https://github.com/xamarin/xamarin-android/issues/1188#issuecomment-358184674
Context: https://github.com/xamarin/xamarin-android/pull/4877
Context: https://github.com/xamarin/xamarin-android/pull/4927#issuecomment-875864999

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.

@jonpryor
Copy link
Member

Ruh roh: Mono.Android.NET_Tests, Xamarin.Android.RuntimeTests.ExceptionTest.InnerExceptionIsSet / Release fails with:

Unable to find the Android.Runtime.JavaProxyThrowable.Create(Exception) method
Expected: not null
But was:  null

Looks like it's being linked away?

MethodInfo? create = JavaProxyThrowable_type.GetMethod (
"Create",
BindingFlags.Static | BindingFlags.Public,
new Type[] { typeof(Exception), typeof(bool) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the "Unable to find ….Create(Exception) method" error, I think it's because it's looking for Create(Exception, bool), which does not exist. We need to instead look for Create(Exception).

@jonpryor jonpryor merged commit 1aa0ea7 into dotnet:main Aug 23, 2023
jonathanpeppers pushed a commit that referenced this pull request Aug 23, 2023
…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.
grendello added a commit to grendello/xamarin-android that referenced this pull request Sep 4, 2023
* main: (57 commits)
  Bump to dotnet/installer@2809943e7a 8.0.100-rc.2.23431.5 (dotnet#8317)
  [build] Use Microsoft OpenJDK 17.0.8 (dotnet#8309)
  [Mono.Android] Add missing `[Flags]` attribute for generated enums. (dotnet#8310)
  Bump to dotnet/installer@c5e45fd659 8.0.100-rc.2.23427.4 (dotnet#8305)
  [xaprepare] Improve dotnet-install script logging (dotnet#8312)
  [xaprepare] Fix dotnet-install script size check (dotnet#8311)
  [Xamarin.Android.Build.Tasks] improve net6.0 "out of support" message (dotnet#8307)
  [monodroid] Fix the EnableNativeAnalyzers build (dotnet#8293)
  Bump to dotnet/installer@56d8c6497c 8.0.100-rc.2.23422.31 (dotnet#8291)
  [Xamarin.Android.Build.Tasks] Fix APT2264 error on Windows. (dotnet#8289)
  [Mono.Android] Marshal .NET stack trace to Throwable.getStackTrace() (dotnet#8185)
  [tests] Skip sign check when installing MAUI (dotnet#8288)
  Bump to xamarin/monodroid@057b17fe (dotnet#8286)
  [Xamarin.Android.Build.Tasks] add $(AndroidStripILAfterAOT) (dotnet#8172)
  Bump to dotnet/installer@ec2c1ec1b1 8.0.100-rc.2.23420.6 (dotnet#8281)
  Bump to dotnet/installer@001d8e4465 8.0.100-rc.2.23417.14 (dotnet#8276)
  [Mono.Android] [IntentFilter] pathSuffix & pathAdvancedPattern  (dotnet#8261)
  $(AndroidPackVersionSuffix)=rc.2; net8 is 34.0.0-rc.2 (dotnet#8278)
  Bump to xamarin/xamarin-android-tools/main@52f0866 (dotnet#8241)
  [build] set file extension for common `ToolExe` values (dotnet#8267)
  ...
grendello added a commit to grendello/xamarin-android that referenced this pull request Sep 4, 2023
* main: (102 commits)
  Bump to dotnet/installer@2809943e7a 8.0.100-rc.2.23431.5 (dotnet#8317)
  [build] Use Microsoft OpenJDK 17.0.8 (dotnet#8309)
  [Mono.Android] Add missing `[Flags]` attribute for generated enums. (dotnet#8310)
  Bump to dotnet/installer@c5e45fd659 8.0.100-rc.2.23427.4 (dotnet#8305)
  [xaprepare] Improve dotnet-install script logging (dotnet#8312)
  [xaprepare] Fix dotnet-install script size check (dotnet#8311)
  [Xamarin.Android.Build.Tasks] improve net6.0 "out of support" message (dotnet#8307)
  [monodroid] Fix the EnableNativeAnalyzers build (dotnet#8293)
  Bump to dotnet/installer@56d8c6497c 8.0.100-rc.2.23422.31 (dotnet#8291)
  [Xamarin.Android.Build.Tasks] Fix APT2264 error on Windows. (dotnet#8289)
  [Mono.Android] Marshal .NET stack trace to Throwable.getStackTrace() (dotnet#8185)
  [tests] Skip sign check when installing MAUI (dotnet#8288)
  Bump to xamarin/monodroid@057b17fe (dotnet#8286)
  [Xamarin.Android.Build.Tasks] add $(AndroidStripILAfterAOT) (dotnet#8172)
  Bump to dotnet/installer@ec2c1ec1b1 8.0.100-rc.2.23420.6 (dotnet#8281)
  Bump to dotnet/installer@001d8e4465 8.0.100-rc.2.23417.14 (dotnet#8276)
  [Mono.Android] [IntentFilter] pathSuffix & pathAdvancedPattern  (dotnet#8261)
  $(AndroidPackVersionSuffix)=rc.2; net8 is 34.0.0-rc.2 (dotnet#8278)
  Bump to xamarin/xamarin-android-tools/main@52f0866 (dotnet#8241)
  [build] set file extension for common `ToolExe` values (dotnet#8267)
  ...
grendello added a commit to grendello/xamarin-android that referenced this pull request Sep 4, 2023
* main: (68 commits)
  Bump to dotnet/installer@2809943e7a 8.0.100-rc.2.23431.5 (dotnet#8317)
  [build] Use Microsoft OpenJDK 17.0.8 (dotnet#8309)
  [Mono.Android] Add missing `[Flags]` attribute for generated enums. (dotnet#8310)
  Bump to dotnet/installer@c5e45fd659 8.0.100-rc.2.23427.4 (dotnet#8305)
  [xaprepare] Improve dotnet-install script logging (dotnet#8312)
  [xaprepare] Fix dotnet-install script size check (dotnet#8311)
  [Xamarin.Android.Build.Tasks] improve net6.0 "out of support" message (dotnet#8307)
  [monodroid] Fix the EnableNativeAnalyzers build (dotnet#8293)
  Bump to dotnet/installer@56d8c6497c 8.0.100-rc.2.23422.31 (dotnet#8291)
  [Xamarin.Android.Build.Tasks] Fix APT2264 error on Windows. (dotnet#8289)
  [Mono.Android] Marshal .NET stack trace to Throwable.getStackTrace() (dotnet#8185)
  [tests] Skip sign check when installing MAUI (dotnet#8288)
  Bump to xamarin/monodroid@057b17fe (dotnet#8286)
  [Xamarin.Android.Build.Tasks] add $(AndroidStripILAfterAOT) (dotnet#8172)
  Bump to dotnet/installer@ec2c1ec1b1 8.0.100-rc.2.23420.6 (dotnet#8281)
  Bump to dotnet/installer@001d8e4465 8.0.100-rc.2.23417.14 (dotnet#8276)
  [Mono.Android] [IntentFilter] pathSuffix & pathAdvancedPattern  (dotnet#8261)
  $(AndroidPackVersionSuffix)=rc.2; net8 is 34.0.0-rc.2 (dotnet#8278)
  Bump to xamarin/xamarin-android-tools/main@52f0866 (dotnet#8241)
  [build] set file extension for common `ToolExe` values (dotnet#8267)
  ...
grendello added a commit to grendello/xamarin-android that referenced this pull request Sep 4, 2023
* main: (53 commits)
  Bump to dotnet/installer@2809943e7a 8.0.100-rc.2.23431.5 (dotnet#8317)
  [build] Use Microsoft OpenJDK 17.0.8 (dotnet#8309)
  [Mono.Android] Add missing `[Flags]` attribute for generated enums. (dotnet#8310)
  Bump to dotnet/installer@c5e45fd659 8.0.100-rc.2.23427.4 (dotnet#8305)
  [xaprepare] Improve dotnet-install script logging (dotnet#8312)
  [xaprepare] Fix dotnet-install script size check (dotnet#8311)
  [Xamarin.Android.Build.Tasks] improve net6.0 "out of support" message (dotnet#8307)
  [monodroid] Fix the EnableNativeAnalyzers build (dotnet#8293)
  Bump to dotnet/installer@56d8c6497c 8.0.100-rc.2.23422.31 (dotnet#8291)
  [Xamarin.Android.Build.Tasks] Fix APT2264 error on Windows. (dotnet#8289)
  [Mono.Android] Marshal .NET stack trace to Throwable.getStackTrace() (dotnet#8185)
  [tests] Skip sign check when installing MAUI (dotnet#8288)
  Bump to xamarin/monodroid@057b17fe (dotnet#8286)
  [Xamarin.Android.Build.Tasks] add $(AndroidStripILAfterAOT) (dotnet#8172)
  Bump to dotnet/installer@ec2c1ec1b1 8.0.100-rc.2.23420.6 (dotnet#8281)
  Bump to dotnet/installer@001d8e4465 8.0.100-rc.2.23417.14 (dotnet#8276)
  [Mono.Android] [IntentFilter] pathSuffix & pathAdvancedPattern  (dotnet#8261)
  $(AndroidPackVersionSuffix)=rc.2; net8 is 34.0.0-rc.2 (dotnet#8278)
  Bump to xamarin/xamarin-android-tools/main@52f0866 (dotnet#8241)
  [build] set file extension for common `ToolExe` values (dotnet#8267)
  ...
grendello added a commit to grendello/xamarin-android that referenced this pull request Sep 4, 2023
* main: (25 commits)
  Bump to dotnet/installer@2809943e7a 8.0.100-rc.2.23431.5 (dotnet#8317)
  [build] Use Microsoft OpenJDK 17.0.8 (dotnet#8309)
  [Mono.Android] Add missing `[Flags]` attribute for generated enums. (dotnet#8310)
  Bump to dotnet/installer@c5e45fd659 8.0.100-rc.2.23427.4 (dotnet#8305)
  [xaprepare] Improve dotnet-install script logging (dotnet#8312)
  [xaprepare] Fix dotnet-install script size check (dotnet#8311)
  [Xamarin.Android.Build.Tasks] improve net6.0 "out of support" message (dotnet#8307)
  [monodroid] Fix the EnableNativeAnalyzers build (dotnet#8293)
  Bump to dotnet/installer@56d8c6497c 8.0.100-rc.2.23422.31 (dotnet#8291)
  [Xamarin.Android.Build.Tasks] Fix APT2264 error on Windows. (dotnet#8289)
  [Mono.Android] Marshal .NET stack trace to Throwable.getStackTrace() (dotnet#8185)
  [tests] Skip sign check when installing MAUI (dotnet#8288)
  Bump to xamarin/monodroid@057b17fe (dotnet#8286)
  [Xamarin.Android.Build.Tasks] add $(AndroidStripILAfterAOT) (dotnet#8172)
  Bump to dotnet/installer@ec2c1ec1b1 8.0.100-rc.2.23420.6 (dotnet#8281)
  Bump to dotnet/installer@001d8e4465 8.0.100-rc.2.23417.14 (dotnet#8276)
  [Mono.Android] [IntentFilter] pathSuffix & pathAdvancedPattern  (dotnet#8261)
  $(AndroidPackVersionSuffix)=rc.2; net8 is 34.0.0-rc.2 (dotnet#8278)
  Bump to xamarin/xamarin-android-tools/main@52f0866 (dotnet#8241)
  [build] set file extension for common `ToolExe` values (dotnet#8267)
  ...
grendello added a commit to grendello/xamarin-android that referenced this pull request Sep 4, 2023
* main: (38 commits)
  Bump to dotnet/installer@2809943e7a 8.0.100-rc.2.23431.5 (dotnet#8317)
  [build] Use Microsoft OpenJDK 17.0.8 (dotnet#8309)
  [Mono.Android] Add missing `[Flags]` attribute for generated enums. (dotnet#8310)
  Bump to dotnet/installer@c5e45fd659 8.0.100-rc.2.23427.4 (dotnet#8305)
  [xaprepare] Improve dotnet-install script logging (dotnet#8312)
  [xaprepare] Fix dotnet-install script size check (dotnet#8311)
  [Xamarin.Android.Build.Tasks] improve net6.0 "out of support" message (dotnet#8307)
  [monodroid] Fix the EnableNativeAnalyzers build (dotnet#8293)
  Bump to dotnet/installer@56d8c6497c 8.0.100-rc.2.23422.31 (dotnet#8291)
  [Xamarin.Android.Build.Tasks] Fix APT2264 error on Windows. (dotnet#8289)
  [Mono.Android] Marshal .NET stack trace to Throwable.getStackTrace() (dotnet#8185)
  [tests] Skip sign check when installing MAUI (dotnet#8288)
  Bump to xamarin/monodroid@057b17fe (dotnet#8286)
  [Xamarin.Android.Build.Tasks] add $(AndroidStripILAfterAOT) (dotnet#8172)
  Bump to dotnet/installer@ec2c1ec1b1 8.0.100-rc.2.23420.6 (dotnet#8281)
  Bump to dotnet/installer@001d8e4465 8.0.100-rc.2.23417.14 (dotnet#8276)
  [Mono.Android] [IntentFilter] pathSuffix & pathAdvancedPattern  (dotnet#8261)
  $(AndroidPackVersionSuffix)=rc.2; net8 is 34.0.0-rc.2 (dotnet#8278)
  Bump to xamarin/xamarin-android-tools/main@52f0866 (dotnet#8241)
  [build] set file extension for common `ToolExe` values (dotnet#8267)
  ...
@github-actions github-actions bot locked and limited conversation to collaborators Jan 22, 2024
@grendello grendello deleted the throwable-stacktrace branch March 27, 2024 11:07
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve JavaProxyThrowable integration
5 participants