Skip to content

Commit 2ccdb27

Browse files
[Mono.Android] Fix UnhandledExceptionRaiser not firing in .NET 10 (#10966)
Context: dotnet/java-interop#1275 Fixes: https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2842209 dotnet/java-interop#1275 eliminated `JNINativeWrapper.CreateDelegate()` from generated binding marshal methods. The old code path routed exceptions through `AndroidEnvironment.UnhandledException()`, which fires the `UnhandledExceptionRaiser` event. The new code path calls `JniRuntime.OnUserUnhandledException()`, which only calls `SetPendingException()` and never invokes `AndroidEnvironment.UnhandledException()`. Fix by overriding `OnUserUnhandledException()` in `AndroidRuntime` to call `AndroidEnvironment.TryRaiseUnhandledException()` before delegating to the base implementation. If a subscriber sets `Handled = true`, the exception is swallowed and not transitioned to JNI. Refactor `AndroidEnvironment.UnhandledException()` to extract the event- raising logic into `TryRaiseUnhandledException()` so it can be called from both the old and new code paths. ~~ Tests ~~ Add a test that verifies both `UnhandledExceptionRaiser` and `AppDomain.UnhandledException` fire when an unhandled exception is thrown from a button click handler. The test: 1. Registers both `UnhandledExceptionRaiser` (with `e.Handled = true`) and `AppDomain.UnhandledException` handlers 2. Throws from the button click delegate 3. Asserts `UnhandledExceptionRaiser` fires via logcat
1 parent 24d6a92 commit 2ccdb27

File tree

3 files changed

+59
-4
lines changed

3 files changed

+59
-4
lines changed

src/Mono.Android/Android.Runtime/AndroidEnvironment.cs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -111,17 +111,25 @@ public static void RaiseThrowable (Java.Lang.Throwable throwable)
111111

112112
internal static void UnhandledException (Exception e)
113113
{
114-
var raisers = UnhandledExceptionRaiser;
114+
if (TryRaiseUnhandledException (e))
115+
return;
116+
117+
RaiseThrowable (Java.Lang.Throwable.FromException (e));
118+
}
119+
120+
// Returns true if the exception was handled by a subscriber.
121+
internal static bool TryRaiseUnhandledException (Exception e)
122+
{
123+
var raisers = UnhandledExceptionRaiser;
115124
if (raisers != null) {
116125
var info = new RaiseThrowableEventArgs (e);
117126
foreach (EventHandler<RaiseThrowableEventArgs> handler in raisers.GetInvocationList ()) {
118127
handler (null, info);
119128
if (info.Handled)
120-
return;
129+
return true;
121130
}
122131
}
123-
124-
RaiseThrowable (Java.Lang.Throwable.FromException (e));
132+
return false;
125133
}
126134

127135
// This is invoked by

src/Mono.Android/Android.Runtime/AndroidRuntime.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,19 @@ public override string GetCurrentManagedThreadStackTrace (int skipFrames, bool f
7373
return peekedExc;
7474
}
7575

76+
public override void OnUserUnhandledException (ref JniTransition transition, Exception e)
77+
{
78+
// Raise the UnhandledExceptionRaiser event via TryRaiseUnhandledException().
79+
// If a subscriber sets Handled = true, the exception is considered handled
80+
// and we return without transitioning to JNI.
81+
// See: https://github.com/dotnet/android/issues/10654
82+
if (AndroidEnvironment.TryRaiseUnhandledException (e)) {
83+
return;
84+
}
85+
86+
base.OnUserUnhandledException (ref transition, e);
87+
}
88+
7689
public override void RaisePendingException (Exception pendingException)
7790
{
7891
var je = pendingException as JavaException;

tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,40 @@ void Reset ()
530530
}
531531
}
532532

533+
[Test]
534+
public void UnhandledExceptionFromButtonClick ([Values (AndroidRuntime.MonoVM, AndroidRuntime.CoreCLR)] AndroidRuntime runtime)
535+
{
536+
proj = new XamarinAndroidApplicationProject ();
537+
proj.SetRuntime (runtime);
538+
proj.SetAndroidSupportedAbis (DeviceAbi);
539+
540+
proj.MainActivity = proj.DefaultMainActivity.Replace ("//${AFTER_ONCREATE}", """
541+
Android.Runtime.AndroidEnvironment.UnhandledExceptionRaiser += (sender, e) => {
542+
Android.Util.Log.Error ("UnhandledTest", $"UnhandledExceptionRaiser: {e.Exception}");
543+
e.Handled = true;
544+
};
545+
546+
button!.Click += (sender, e) => {
547+
throw new Exception ("Unhandled exception test");
548+
};
549+
""");
550+
551+
builder = CreateApkBuilder ();
552+
Assert.IsTrue (builder.Install (proj), "Install should have succeeded.");
553+
AdbStartActivity ($"{proj.PackageName}/{proj.JavaPackageName}.MainActivity");
554+
Assert.IsTrue (WaitForActivityToStart (proj.PackageName, "MainActivity",
555+
Path.Combine (Root, builder.ProjectDirectory, "startup-logcat.log")), "Activity should have started.");
556+
ClearAdbLogcat ();
557+
ClearBlockingDialogs ();
558+
ClickButton (proj.PackageName, "myButton", "MY BUTTON");
559+
560+
string expectedRaiser = "UnhandledExceptionRaiser: System.Exception: Unhandled exception test";
561+
Assert.IsTrue (
562+
MonitorAdbLogcat (CreateLineChecker (expectedRaiser),
563+
logcatFilePath: Path.Combine (Root, builder.ProjectDirectory, "unhandled-logcat.log"), timeout: 60),
564+
$"Output did not contain {expectedRaiser}!");
565+
}
566+
533567
[Test]
534568
[Category ("UsesDevice")]
535569
[TestCase ("テスト")]

0 commit comments

Comments
 (0)