diff --git a/osu.Android/CrashDiagnostics.cs b/osu.Android/CrashDiagnostics.cs index 65587a30d7ba..4abf6f0f9111 100644 --- a/osu.Android/CrashDiagnostics.cs +++ b/osu.Android/CrashDiagnostics.cs @@ -199,13 +199,24 @@ public static void InstallManagedExceptionHooks() // AppDomain.UnhandledException before aborting — so without this hook the // exception that ultimately kills the process can vanish without trace. We // record it here on every throw so the *last* recorded exception before a - // SIGSEGV/SIGABRT is the candidate culprit. To avoid drowning the log in noise - // we filter by exception type — only fatal-ish kinds are recorded. + // SIGSEGV/SIGABRT is the candidate culprit. + // + // Filtering policy: + // * On osu.Framework GameThreads (Draw/Update/Audio/Input): log *every* + // exception. An unhandled throw on any of these threads will tear down + // the process via Mono's tgkill(SIGSEGV) path with no managed trace + // reaching AppDomain.UnhandledException, so we cannot afford to filter. + // * On all other threads: keep the legacy "fatal-ish kinds" type filter + // so the log is not flooded by routine first-chance noise (e.g. the + // HidSharp / CFStringCreateWithCharacters EntryPointNotFoundException + // that fires every startup on .NET TP Worker). try { AppDomain.CurrentDomain.FirstChanceException += (_, e) => { - if (e.Exception is NullReferenceException + bool isGameThread = isOsuGameThread(Thread.CurrentThread.Name); + + bool isFatalKind = e.Exception is NullReferenceException or AccessViolationException or StackOverflowException or TypeInitializationException @@ -215,7 +226,9 @@ or BadImageFormatException or TypeLoadException or MissingMethodException or MissingFieldException - or InvalidProgramException) + or InvalidProgramException; + + if (isGameThread || isFatalKind) { writeManagedException($"FirstChanceException ({e.Exception.GetType().Name})", e.Exception); } @@ -227,6 +240,23 @@ or MissingFieldException } } + // osu.Framework names its game threads with stable prefixes such as + // "DrawThread", "UpdateThread", "AudioThread", "InputThread", and the + // tombstone we are debugging shows the comm name "Draw (GameThread)". + // Match any of these so an exception thrown deep inside the renderer or + // audio pipeline gets captured before Mono aborts the process. + private static bool isOsuGameThread(string? name) + { + if (string.IsNullOrEmpty(name)) + return false; + + return name.StartsWith("Draw", StringComparison.Ordinal) + || name.StartsWith("Update", StringComparison.Ordinal) + || name.StartsWith("Audio", StringComparison.Ordinal) + || name.StartsWith("Input", StringComparison.Ordinal) + || name.Contains("GameThread", StringComparison.Ordinal); + } + /// /// Records a one-line summary of the native handler install state (sentinel exists? /// log path?) so the very first thing we see in the log on the next inspection tells