|
| 1 | +// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. |
| 2 | +// See the LICENCE file in the repository root for full licence text. |
| 3 | + |
| 4 | +using System; |
| 5 | +using System.Runtime.InteropServices; |
| 6 | +using Debug = System.Diagnostics.Debug; |
| 7 | + |
| 8 | +namespace osu.Android.Native |
| 9 | +{ |
| 10 | + /// <summary> |
| 11 | + /// Managed-side wrapper for the native pthread liveness watchdog implemented in |
| 12 | + /// <c>osu.Android/Native/native_watchdog.cpp</c>. |
| 13 | + /// |
| 14 | + /// <para> |
| 15 | + /// The native watchdog exists because the managed <see cref="osu.Android.HangWatchdog"/> |
| 16 | + /// runs as a normal <c>System.Threading.Thread</c>, which Mono suspends during a |
| 17 | + /// stop-the-world GC by sending <c>SIGRTMIN+N</c>. If a Mono thread is stuck |
| 18 | + /// inside a long native call (Vulkan present-queue futex, Realm fifo open, |
| 19 | + /// AAudio init, …) the STW request never completes and every other managed |
| 20 | + /// thread — including our managed watchdog's monitor — is parked indefinitely. |
| 21 | + /// A pure pthread-only watchdog that never attaches to Mono is the only thing |
| 22 | + /// that can produce a diagnostic dump under that condition. |
| 23 | + /// </para> |
| 24 | + /// |
| 25 | + /// <para> |
| 26 | + /// All entry points are best-effort: a missing native library or a |
| 27 | + /// <see cref="DllNotFoundException"/> is non-fatal and silently downgraded to |
| 28 | + /// a <see cref="Debug.WriteLine"/> call so startup is unaffected. |
| 29 | + /// </para> |
| 30 | + /// </summary> |
| 31 | + internal static class NativeWatchdog |
| 32 | + { |
| 33 | + // Same lib name used by OboeAudioBridge — single shared libosu_native.so. |
| 34 | + private const string lib_name = "osu_native"; |
| 35 | + |
| 36 | + /// <summary> |
| 37 | + /// Arm the native watchdog with the given log path and hang threshold. |
| 38 | + /// Idempotent: subsequent calls are no-ops on the native side. |
| 39 | + /// Never throws. |
| 40 | + /// </summary> |
| 41 | + /// <param name="logPath">Absolute path of the file to append hang dumps to (typically <c>FilesDir/native_crash.log</c>).</param> |
| 42 | + /// <param name="hangSeconds">Seconds without a heartbeat before a dump is triggered. Native side clamps to [3, 120].</param> |
| 43 | + public static void Start(string? logPath, int hangSeconds) |
| 44 | + { |
| 45 | + try |
| 46 | + { |
| 47 | + osu_native_watchdog_start(logPath, hangSeconds); |
| 48 | + } |
| 49 | + catch (DllNotFoundException e) |
| 50 | + { |
| 51 | + Debug.WriteLine($"[osu!] NativeWatchdog.Start: libosu_native.so not loaded, watchdog disabled ({e.Message})"); |
| 52 | + } |
| 53 | + catch (EntryPointNotFoundException e) |
| 54 | + { |
| 55 | + // The native entry point is absent — most likely an old libosu_native.so |
| 56 | + // in the APK that does not include native_watchdog.cpp. Treat as disabled |
| 57 | + // rather than crashing the user's startup. |
| 58 | + Debug.WriteLine($"[osu!] NativeWatchdog.Start: entry point missing, watchdog disabled ({e.Message})"); |
| 59 | + } |
| 60 | + catch (Exception e) |
| 61 | + { |
| 62 | + Debug.WriteLine($"[osu!] NativeWatchdog.Start unexpected failure: {e.Message}"); |
| 63 | + } |
| 64 | + } |
| 65 | + |
| 66 | + /// <summary> |
| 67 | + /// Bump the native heartbeat. Called from the managed |
| 68 | + /// <see cref="osu.Android.HangWatchdog"/> per-thread tick so the native watchdog |
| 69 | + /// can observe Update-thread liveness across Mono STW pauses. The underlying |
| 70 | + /// native call performs a single <c>__atomic_store_n</c> on a 64-bit slot; |
| 71 | + /// safe to call at any rate, from any thread, without locking. |
| 72 | + /// Never throws. |
| 73 | + /// </summary> |
| 74 | + public static void Heartbeat() |
| 75 | + { |
| 76 | + try |
| 77 | + { |
| 78 | + osu_native_watchdog_heartbeat(); |
| 79 | + } |
| 80 | + catch (DllNotFoundException) { /* watchdog disabled — no-op */ } |
| 81 | + catch (EntryPointNotFoundException) { /* old libosu_native.so — no-op */ } |
| 82 | + catch (Exception e) |
| 83 | + { |
| 84 | + // Heartbeat is on the GameThread tick path; never let a diagnostic |
| 85 | + // failure escape into the game loop. |
| 86 | + Debug.WriteLine($"[osu!] NativeWatchdog.Heartbeat unexpected failure: {e.Message}"); |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + [DllImport(lib_name)] |
| 91 | + private static extern void osu_native_watchdog_start([MarshalAs(UnmanagedType.LPUTF8Str)] string? logPath, int hangSeconds); |
| 92 | + |
| 93 | + [DllImport(lib_name)] |
| 94 | + private static extern void osu_native_watchdog_heartbeat(); |
| 95 | + } |
| 96 | +} |
0 commit comments