From a0b858ad479fcbdb245797ec6a151c832971a3d9 Mon Sep 17 00:00:00 2001 From: Rolf Bjarne Kvinge Date: Wed, 29 May 2024 20:14:47 +0200 Subject: [PATCH] [runtime] Call mono_unhandled_exception to raise AppDomain.UnhandledException. (#20656) Call mono_unhandled_exception to raise AppDomain.UnhandledException when managed exceptions are unhandled. Partial fix for #15252 (for MonoVM, still pending for CoreCLR, which needs https://github.com/dotnet/runtime/issues/102730 fixed first). --- runtime/coreclr-bridge.m | 7 +++ runtime/exports.t4 | 6 +++ runtime/monovm-bridge.m | 7 +++ runtime/runtime.m | 10 +++++ runtime/xamarin/runtime.h | 1 + .../dotnet/ExceptionalTestApp/AppDelegate.cs | 44 +++++++++++++++++++ .../MacCatalyst/ExceptionalTestApp.csproj | 7 +++ .../ExceptionalTestApp/MacCatalyst/Makefile | 1 + tests/dotnet/ExceptionalTestApp/Makefile | 2 + .../iOS/ExceptionalTestApp.csproj | 7 +++ tests/dotnet/ExceptionalTestApp/iOS/Makefile | 1 + .../macOS/ExceptionalTestApp.csproj | 7 +++ .../dotnet/ExceptionalTestApp/macOS/Makefile | 1 + tests/dotnet/ExceptionalTestApp/shared.csproj | 15 +++++++ tests/dotnet/ExceptionalTestApp/shared.mk | 3 ++ .../tvOS/ExceptionalTestApp.csproj | 7 +++ tests/dotnet/ExceptionalTestApp/tvOS/Makefile | 1 + tests/dotnet/UnitTests/ProjectTest.cs | 26 +++++++++++ tests/dotnet/UnitTests/TestBaseClass.cs | 14 +++--- 19 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 tests/dotnet/ExceptionalTestApp/AppDelegate.cs create mode 100644 tests/dotnet/ExceptionalTestApp/MacCatalyst/ExceptionalTestApp.csproj create mode 100644 tests/dotnet/ExceptionalTestApp/MacCatalyst/Makefile create mode 100644 tests/dotnet/ExceptionalTestApp/Makefile create mode 100644 tests/dotnet/ExceptionalTestApp/iOS/ExceptionalTestApp.csproj create mode 100644 tests/dotnet/ExceptionalTestApp/iOS/Makefile create mode 100644 tests/dotnet/ExceptionalTestApp/macOS/ExceptionalTestApp.csproj create mode 100644 tests/dotnet/ExceptionalTestApp/macOS/Makefile create mode 100644 tests/dotnet/ExceptionalTestApp/shared.csproj create mode 100644 tests/dotnet/ExceptionalTestApp/shared.mk create mode 100644 tests/dotnet/ExceptionalTestApp/tvOS/ExceptionalTestApp.csproj create mode 100644 tests/dotnet/ExceptionalTestApp/tvOS/Makefile diff --git a/runtime/coreclr-bridge.m b/runtime/coreclr-bridge.m index 97a634721ae3..b3de7428d1a6 100644 --- a/runtime/coreclr-bridge.m +++ b/runtime/coreclr-bridge.m @@ -1174,4 +1174,11 @@ return rv; } +void +xamarin_bridge_raise_unhandled_exception_event (GCHandle exception_gchandle) +{ + // There's no way to raise the AppDomain.UnhandledException event. + // https://github.com/dotnet/runtime/issues/102730 +} + #endif // CORECLR_RUNTIME diff --git a/runtime/exports.t4 b/runtime/exports.t4 index 90c18c0d3576..8808b93e9812 100644 --- a/runtime/exports.t4 +++ b/runtime/exports.t4 @@ -169,6 +169,12 @@ XamarinRuntime = RuntimeMode.MonoVM, }, + new Export ("void", "mono_unhandled_exception", + "MonoObject *", "ex" + ) { + XamarinRuntime = RuntimeMode.MonoVM, + }, + new Export ("char*", "mono_array_addr_with_size", "MonoArray *", "array", "int", "size", diff --git a/runtime/monovm-bridge.m b/runtime/monovm-bridge.m index 904dae8acfe6..a538f1cf61e5 100644 --- a/runtime/monovm-bridge.m +++ b/runtime/monovm-bridge.m @@ -563,4 +563,11 @@ mono_profiler_install_gc (gc_event_callback, NULL); } +void +xamarin_bridge_raise_unhandled_exception_event (GCHandle exception_gchandle) +{ + MonoObject *exc = xamarin_gchandle_get_target (exception_gchandle); + mono_unhandled_exception (exc); +} + #endif // !CORECLR_RUNTIME diff --git a/runtime/runtime.m b/runtime/runtime.m index 97090b9c646e..6e4c753663e7 100644 --- a/runtime/runtime.m +++ b/runtime/runtime.m @@ -1143,6 +1143,16 @@ -(void) xamarinSetFlags: (enum XamarinGCHandleFlags) flags; // COOP: We won't get here in coop-mode, because we don't set the uncaught objc exception handler in that case. LOG (PRODUCT ": Received unhandled ObjectiveC exception: %@ %@", [exc name], [exc reason]); + XamarinGCHandle* exc_handle = [[exc userInfo] objectForKey: @"XamarinManagedExceptionHandle"]; + if (exc_handle != NULL) { + GCHandle exception_gchandle = [exc_handle getHandle]; + if (exception_gchandle != INVALID_GCHANDLE) { + xamarin_bridge_raise_unhandled_exception_event (exception_gchandle); + PRINT ("Received unhandled Objective-C exception that was marshalled from a managed exception: %@", exc); + abort (); + } + } + if (xamarin_handling_unhandled_exceptions == 1) { PRINT ("Detected recursion when handling uncaught Objective-C exception: %@", exc); abort (); diff --git a/runtime/xamarin/runtime.h b/runtime/xamarin/runtime.h index 63cb4ddf36b8..252319144614 100644 --- a/runtime/xamarin/runtime.h +++ b/runtime/xamarin/runtime.h @@ -223,6 +223,7 @@ void xamarin_bridge_call_runtime_initialize (struct InitializationOptions* opt void xamarin_bridge_register_product_assembly (GCHandle* exception_gchandle); MonoMethod * xamarin_bridge_get_mono_method (MonoReflectionMethod *method); void xamarin_bridge_free_mono_signature (MonoMethodSignature **signature); +void xamarin_bridge_raise_unhandled_exception_event (GCHandle exception_gchandle); // the GCHandle is *not* freed. This method will return after raising the event. bool xamarin_register_monoassembly (MonoAssembly *assembly, GCHandle *exception_gchandle); void xamarin_install_nsautoreleasepool_hooks (); void xamarin_enable_new_refcount (); diff --git a/tests/dotnet/ExceptionalTestApp/AppDelegate.cs b/tests/dotnet/ExceptionalTestApp/AppDelegate.cs new file mode 100644 index 000000000000..5df9c1a02847 --- /dev/null +++ b/tests/dotnet/ExceptionalTestApp/AppDelegate.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.InteropServices; + +using Foundation; + +namespace MySimpleApp { + public class Program { + static int Main (string [] args) + { + GC.KeepAlive (typeof (NSObject)); // prevent linking away the platform assembly + + var testCaseString = Environment.GetEnvironmentVariable ("EXCEPTIONAL_TEST_CASE"); + if (string.IsNullOrEmpty (testCaseString)) { + Console.WriteLine ($"The environment variable EXCEPTIONAL_TEST_CASE wasn't set."); + return 2; + } + var testCase = int.Parse (testCaseString); + switch (testCase) { + case 1: + AppDomain.CurrentDomain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) => { + if (e.ExceptionObject is TestCaseException) { + Console.WriteLine (Environment.GetEnvironmentVariable ("MAGIC_WORD")); + } else { + Console.WriteLine ($"Unexpected exception type: {e.ExceptionObject?.GetType ()}"); + } + Environment.Exit (0); + }; + throw new TestCaseException (); + default: + Console.WriteLine ($"Unknown test case: {testCase}"); + return 3; + } + + return 1; + } + } +} + +class TestCaseException : Exception { + public TestCaseException () + : base ("Testing, testing") + { + } +} diff --git a/tests/dotnet/ExceptionalTestApp/MacCatalyst/ExceptionalTestApp.csproj b/tests/dotnet/ExceptionalTestApp/MacCatalyst/ExceptionalTestApp.csproj new file mode 100644 index 000000000000..6b0e2c773180 --- /dev/null +++ b/tests/dotnet/ExceptionalTestApp/MacCatalyst/ExceptionalTestApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-maccatalyst + + + diff --git a/tests/dotnet/ExceptionalTestApp/MacCatalyst/Makefile b/tests/dotnet/ExceptionalTestApp/MacCatalyst/Makefile new file mode 100644 index 000000000000..110d078f4577 --- /dev/null +++ b/tests/dotnet/ExceptionalTestApp/MacCatalyst/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/ExceptionalTestApp/Makefile b/tests/dotnet/ExceptionalTestApp/Makefile new file mode 100644 index 000000000000..6affa45ff122 --- /dev/null +++ b/tests/dotnet/ExceptionalTestApp/Makefile @@ -0,0 +1,2 @@ +TOP=../../.. +include $(TOP)/tests/common/shared-dotnet-test.mk diff --git a/tests/dotnet/ExceptionalTestApp/iOS/ExceptionalTestApp.csproj b/tests/dotnet/ExceptionalTestApp/iOS/ExceptionalTestApp.csproj new file mode 100644 index 000000000000..86d408734aa8 --- /dev/null +++ b/tests/dotnet/ExceptionalTestApp/iOS/ExceptionalTestApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-ios + + + diff --git a/tests/dotnet/ExceptionalTestApp/iOS/Makefile b/tests/dotnet/ExceptionalTestApp/iOS/Makefile new file mode 100644 index 000000000000..110d078f4577 --- /dev/null +++ b/tests/dotnet/ExceptionalTestApp/iOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/ExceptionalTestApp/macOS/ExceptionalTestApp.csproj b/tests/dotnet/ExceptionalTestApp/macOS/ExceptionalTestApp.csproj new file mode 100644 index 000000000000..a77287b9ba00 --- /dev/null +++ b/tests/dotnet/ExceptionalTestApp/macOS/ExceptionalTestApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-macos + + + diff --git a/tests/dotnet/ExceptionalTestApp/macOS/Makefile b/tests/dotnet/ExceptionalTestApp/macOS/Makefile new file mode 100644 index 000000000000..110d078f4577 --- /dev/null +++ b/tests/dotnet/ExceptionalTestApp/macOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/ExceptionalTestApp/shared.csproj b/tests/dotnet/ExceptionalTestApp/shared.csproj new file mode 100644 index 000000000000..b86b58f7725c --- /dev/null +++ b/tests/dotnet/ExceptionalTestApp/shared.csproj @@ -0,0 +1,15 @@ + + + + Exe + + ExceptionalTestApp + com.xamarin.exceptionaltestapp + + + + + + + + diff --git a/tests/dotnet/ExceptionalTestApp/shared.mk b/tests/dotnet/ExceptionalTestApp/shared.mk new file mode 100644 index 000000000000..5b273b7fe0e6 --- /dev/null +++ b/tests/dotnet/ExceptionalTestApp/shared.mk @@ -0,0 +1,3 @@ +TOP=../../../.. +TESTNAME=MySimpleApp +include $(TOP)/tests/common/shared-dotnet.mk diff --git a/tests/dotnet/ExceptionalTestApp/tvOS/ExceptionalTestApp.csproj b/tests/dotnet/ExceptionalTestApp/tvOS/ExceptionalTestApp.csproj new file mode 100644 index 000000000000..bd487ddcd88d --- /dev/null +++ b/tests/dotnet/ExceptionalTestApp/tvOS/ExceptionalTestApp.csproj @@ -0,0 +1,7 @@ + + + + net$(BundledNETCoreAppTargetFrameworkVersion)-tvos + + + diff --git a/tests/dotnet/ExceptionalTestApp/tvOS/Makefile b/tests/dotnet/ExceptionalTestApp/tvOS/Makefile new file mode 100644 index 000000000000..110d078f4577 --- /dev/null +++ b/tests/dotnet/ExceptionalTestApp/tvOS/Makefile @@ -0,0 +1 @@ +include ../shared.mk diff --git a/tests/dotnet/UnitTests/ProjectTest.cs b/tests/dotnet/UnitTests/ProjectTest.cs index b562a4f5732b..07595dc4146f 100644 --- a/tests/dotnet/UnitTests/ProjectTest.cs +++ b/tests/dotnet/UnitTests/ProjectTest.cs @@ -1764,5 +1764,31 @@ public void SourcelinkTest (ApplePlatform platform, string runtimeIdentifiers, s Assert.AreEqual ($"sourcelink test passed: {pdbFile}", test.StandardOutput.ToString ().TrimEnd ('\n')); } + + + [Test] + // [TestCase (ApplePlatform.iOS)] // Skipping because we're not executing tvOS apps anyway (but it should work) + // [TestCase (ApplePlatform.TVOS)] // Skipping because we're not executing tvOS apps anyway (but it should work) + [TestCase (ApplePlatform.MacOSX)] // https://github.com/dotnet/runtime/issues/102730 + [TestCase (ApplePlatform.MacCatalyst)] + public void RaisesAppDomainUnhandledExceptionEvent (ApplePlatform platform) + { + var project = "ExceptionalTestApp"; + Configuration.IgnoreIfIgnoredPlatform (platform); + + var runtimeIdentifiers = GetDefaultRuntimeIdentifier (platform); + var project_path = GetProjectPath (project, runtimeIdentifiers: runtimeIdentifiers, platform: platform, out var appPath); + Clean (project_path); + var properties = GetDefaultProperties (); + DotNet.AssertBuild (project_path, properties); + + if (CanExecute (platform, runtimeIdentifiers)) { + var env = new Dictionary { + { "EXCEPTIONAL_TEST_CASE", "1" }, + }; + var appExecutable = GetNativeExecutable (platform, appPath); + var output = ExecuteWithMagicWordAndAssert (appExecutable, env); + } + } } } diff --git a/tests/dotnet/UnitTests/TestBaseClass.cs b/tests/dotnet/UnitTests/TestBaseClass.cs index aac1e79a9c5c..b432e0fb4309 100644 --- a/tests/dotnet/UnitTests/TestBaseClass.cs +++ b/tests/dotnet/UnitTests/TestBaseClass.cs @@ -327,28 +327,28 @@ protected string GenerateProject (ApplePlatform platform, string name, string ru return csproj; } - protected string ExecuteWithMagicWordAndAssert (ApplePlatform platform, string runtimeIdentifiers, string executable) + protected string ExecuteWithMagicWordAndAssert (ApplePlatform platform, string runtimeIdentifiers, string executable, Dictionary? environment = null) { if (!CanExecute (platform, runtimeIdentifiers)) return string.Empty; - return ExecuteWithMagicWordAndAssert (executable); + return ExecuteWithMagicWordAndAssert (executable, environment); } - protected string ExecuteWithMagicWordAndAssert (string executable) + protected string ExecuteWithMagicWordAndAssert (string executable, Dictionary? environment = null) { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { Console.WriteLine ($"Not executing '{executable}' because we're on Windows."); return string.Empty; } - var rv = Execute (executable, out var output, out string magicWord); + var rv = Execute (executable, out var output, out string magicWord, environment); Assert.That (output.ToString (), Does.Contain (magicWord), "Contains magic word"); Assert.AreEqual (0, rv.ExitCode, "ExitCode"); return output.ToString (); } - protected Execution Execute (string executable, out StringBuilder output, out string magicWord) + protected Execution Execute (string executable, out StringBuilder output, out string magicWord, Dictionary? environment = null) { if (!File.Exists (executable)) throw new FileNotFoundException ($"The executable '{executable}' does not exists."); @@ -358,6 +358,10 @@ protected Execution Execute (string executable, out StringBuilder output, out st { "MAGIC_WORD", magicWord }, { "DYLD_FALLBACK_LIBRARY_PATH", null }, // VSMac might set this, which may cause tests to crash. }; + if (environment is not null) { + foreach (var kvp in environment) + env [kvp.Key] = kvp.Value; + } output = new StringBuilder (); return Execution.RunWithStringBuildersAsync (executable, Array.Empty (), environment: env, standardOutput: output, standardError: output, timeout: TimeSpan.FromSeconds (15)).Result;