diff --git a/Explorer/Assets/DCL/Chat/Commands/AnrDumpChatCommand.cs b/Explorer/Assets/DCL/Chat/Commands/AnrDumpChatCommand.cs new file mode 100644 index 00000000000..e54c1f6b429 --- /dev/null +++ b/Explorer/Assets/DCL/Chat/Commands/AnrDumpChatCommand.cs @@ -0,0 +1,29 @@ +#if UNITY_STANDALONE_WIN +using Cysharp.Threading.Tasks; +using DCL.Diagnostics.Sentry; +using RichTypes; +using System.Threading; +using Cysharp.Threading.Tasks; + +namespace DCL.Chat.Commands +{ + public class AnrDumpChatCommand : IChatCommand + { + public string Command => "anr-dump"; + public string Description => "/anr-dump\n Collect and archive a process dump to the app directory"; + public bool DebugOnly => true; + + public async UniTask ExecuteCommandAsync(string[] parameters, CancellationToken ct) + { + Result<(string filePath, string zipPath)> result = default; + { + await using var _ = await global::Utility.Multithreading.ExecuteOnThreadPoolScope.NewScopeAsync(); + result = ThreadsDumpUtility.CollectAndArchiveDumpInfoToAppDir(); + } + + if (result.Success == false) return $"Dump failed: {result.ErrorMessage}"; + return $"Dump collected:\n {result.Value.filePath}\n {result.Value.zipPath}"; + } + } +} +#endif diff --git a/Explorer/Assets/DCL/Chat/Commands/AnrDumpChatCommand.cs.meta b/Explorer/Assets/DCL/Chat/Commands/AnrDumpChatCommand.cs.meta new file mode 100644 index 00000000000..29f8ac350cc --- /dev/null +++ b/Explorer/Assets/DCL/Chat/Commands/AnrDumpChatCommand.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 74c81c0dc45464941b214eddef2ac570 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Chat/Commands/AnrSimulateChatCommand.cs b/Explorer/Assets/DCL/Chat/Commands/AnrSimulateChatCommand.cs new file mode 100644 index 00000000000..03d14131a74 --- /dev/null +++ b/Explorer/Assets/DCL/Chat/Commands/AnrSimulateChatCommand.cs @@ -0,0 +1,32 @@ +using Cysharp.Threading.Tasks; +using System.Globalization; +using System.Threading; + +namespace DCL.Chat.Commands +{ + public class AnrSimulateChatCommand : IChatCommand + { + private const int DEFAULT_FREEZE_MS = 10_000; + + public string Command => "anr-simulate"; + public string Description => "/anr-simulate [ms]\n Freeze the main thread to trigger ANR detection"; + public bool DebugOnly => true; + + public bool ValidateParameters(string[] parameters) => + parameters.Length == 0 || (parameters.Length == 1 && int.TryParse(parameters[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out _)); + + public UniTask ExecuteCommandAsync(string[] parameters, CancellationToken ct) + { + int freezeMs = DEFAULT_FREEZE_MS; + + if (parameters.Length == 1) + int.TryParse(parameters[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out freezeMs); + +#if !UNITY_WEBGL + Thread.Sleep(freezeMs); // IGNORE_LINE_WEBGL_THREAD_SAFETY_FLAG +#endif + + return UniTask.FromResult($"Main thread was frozen for {freezeMs} ms."); + } + } +} diff --git a/Explorer/Assets/DCL/Chat/Commands/AnrSimulateChatCommand.cs.meta b/Explorer/Assets/DCL/Chat/Commands/AnrSimulateChatCommand.cs.meta new file mode 100644 index 00000000000..c8c75c11af3 --- /dev/null +++ b/Explorer/Assets/DCL/Chat/Commands/AnrSimulateChatCommand.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 0bb7b50941837d14e94bab30399dbf61 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Chat/MessageBus/CommandsHandleChatMessageBus.cs b/Explorer/Assets/DCL/Chat/MessageBus/CommandsHandleChatMessageBus.cs index 9256f8861d7..ca22cd48611 100644 --- a/Explorer/Assets/DCL/Chat/MessageBus/CommandsHandleChatMessageBus.cs +++ b/Explorer/Assets/DCL/Chat/MessageBus/CommandsHandleChatMessageBus.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using Utility; +using DCL.Diagnostics; namespace DCL.Chat.MessageBus { @@ -69,7 +70,11 @@ private async UniTaskVoid HandleChatCommandAsync(ChatChannel.ChannelId channelId string response = await command.ExecuteCommandAsync(parameters, commandCts.Token); SendFromSystem(channelId, channelType, response); } - catch (Exception) { SendFromSystem(channelId, channelType, "🔴 Error running command."); } + catch (Exception e) + { + SendFromSystem(channelId, channelType, "🔴 Error running command."); + ReportHub.LogError(ReportCategory.UNSPECIFIED, $"Error running command: {e}"); + } return; } diff --git a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/ApplicationParametersParser.cs b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/ApplicationParametersParser.cs index f0915c72f07..6c3cbdd1361 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/ApplicationParametersParser.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/AppArgs/ApplicationParametersParser.cs @@ -130,13 +130,16 @@ private void LogArguments() var sb = new StringBuilder(COUNT_PER_LINE * appParameters.Count); var count = 1; + sb.AppendLine("=================="); sb.AppendLine("Application arguments:"); + sb.AppendLine("==================\n"); foreach ((string? key, string? value) in appParameters) { sb.Append("Arg ").Append(count).Append(": ").Append(key).Append(" = ").Append(value).Append("\n"); count++; } + sb.AppendLine("==================\n"); ReportHub.LogProductionInfo(sb.ToString()); } diff --git a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs index 70b731e977d..4ecccb2482a 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs @@ -594,6 +594,10 @@ await MapRendererContainer new SceneAdminsChatCommand(), new AppArgsCommand(appArgs), new LogMatrixChatCommand((RuntimeReportsHandlingSettings)bootstrapContainer.DiagnosticsContainer.Settings), + new AnrSimulateChatCommand(), +#if UNITY_STANDALONE_WIN + new AnrDumpChatCommand(), +#endif }; chatCommands.Add(new HelpChatCommand(chatCommands, appArgs)); diff --git a/Explorer/Assets/DCL/Infrastructure/Utility/Utility.asmdef b/Explorer/Assets/DCL/Infrastructure/Utility/Utility.asmdef index 4d12bb074bb..f0c8c8f16d8 100644 --- a/Explorer/Assets/DCL/Infrastructure/Utility/Utility.asmdef +++ b/Explorer/Assets/DCL/Infrastructure/Utility/Utility.asmdef @@ -14,7 +14,9 @@ "GUID:9887bf5401cdc9140916d3edbea10b69", "GUID:ba053ae967dabc94a811350e36a486f3", "GUID:3c7b57a14671040bd8c549056adc04f5", - "GUID:2efe6c760f205482eb96395a31bb7036" + "GUID:2efe6c760f205482eb96395a31bb7036", + "GUID:1087662aaf1c5462baa91fb9484296fd", + "GUID:75edf6fa50ff464395ca31529ea14d25" ], "includePlatforms": [], "excludePlatforms": [], @@ -36,4 +38,4 @@ "defineConstraints": [], "versionDefines": [], "noEngineReferences": false -} +} \ No newline at end of file diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrException.cs b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrException.cs new file mode 100644 index 00000000000..4ddf8827ecf --- /dev/null +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrException.cs @@ -0,0 +1,18 @@ +using System; + +namespace DCL.Diagnostics.Sentry +{ + public class DclApplicationNotRespondingException : Exception + { +#if UNITY_STANDALONE_WIN + public readonly string? DumpFilePath; + + internal DclApplicationNotRespondingException(string message, string? dumpFilePath) : base(message) + { + this.DumpFilePath = dumpFilePath; + } +#else + internal DclApplicationNotRespondingException(string message) : base(message) { } +#endif + } +} diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrException.cs.meta b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrException.cs.meta new file mode 100644 index 00000000000..f3d34f3658d --- /dev/null +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrException.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: da127028eb58548549d509cce4e4530f \ No newline at end of file diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrIntegration.cs b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrIntegration.cs new file mode 100644 index 00000000000..a97cef504da --- /dev/null +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrIntegration.cs @@ -0,0 +1,605 @@ +// Based on AnrIntegration +// TRUST_WEBGL_THREAD_SAFETY_FLAG +#if !UNITY_WEBGL + +using System; +using System.Collections; +using System.Diagnostics; +using System.Reflection; +using System.Threading; +using Sentry; +using Sentry.Extensibility; +using Sentry.Integrations; +using Sentry.Unity; +using Sentry.Unity.Integrations; +using Debug = UnityEngine.Debug; +using RichTypes; +using System.IO; +using System.IO.Compression; +using DCL.Utility; + +using System.ComponentModel; +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace DCL.Diagnostics.Sentry +{ + internal class DclAnrIntegration : ISdkIntegration + { + private static readonly object Lock = new(); + private static DclAnrWatchDog? Watchdog; + private readonly SentryMonoBehaviour _monoBehaviour; + + public DclAnrIntegration(SentryMonoBehaviour monoBehaviour) + { + _monoBehaviour = monoBehaviour; + } + + public void Register(IHub hub, SentryOptions sentryOptions) + { + var options = (SentryUnityOptions)sentryOptions; + lock (Lock) + { + if (Watchdog is null) + { + // Use multithreaded version for Desktop +#if !UNITY_WEBGL + Watchdog = new DclAnrWatchDogMultiThreaded(options.DiagnosticLogger, + _monoBehaviour, + options.AnrTimeout); +#else + Watchdog = new DclAnrWatchDogSingleThreaded(options.DiagnosticLogger, + _monoBehaviour, + options.AnrTimeout); +#endif + } + } + + Watchdog.OnApplicationNotResponding += (_, e) => + { + SentryEvent se = new SentryEvent(e); +#if UNITY_STANDALONE_WIN + hub.CaptureEvent(se, scope => + { + string? filePath = e.DumpFilePath; + if (filePath != null) + { + scope.AddAttachment(filePath: filePath, AttachmentType.Default); + } + }); +#else + hub.CaptureEvent(se); +#endif + }; + + + } + } + + internal abstract class DclAnrWatchDog + { + public const string Mechanism = "MainThreadWatchdog"; + + protected readonly int DetectionTimeoutMs; + // Note: we don't sleep for the whole detection timeout or we wouldn't capture if the ANR started later. + protected readonly int SleepIntervalMs; + protected readonly IDiagnosticLogger? Logger; + protected readonly SentryMonoBehaviour MonoBehaviour; + internal event EventHandler OnApplicationNotResponding = delegate { }; + protected bool Paused { get; private set; } = false; + + internal DclAnrWatchDog(IDiagnosticLogger? logger, SentryMonoBehaviour monoBehaviour, TimeSpan detectionTimeout) + { + MonoBehaviour = monoBehaviour; + Logger = logger; + DetectionTimeoutMs = (int)detectionTimeout.TotalMilliseconds; + SleepIntervalMs = Math.Max(1, DetectionTimeoutMs / 5); + + MonoBehaviour.ApplicationPausing += () => Paused = true; + MonoBehaviour.ApplicationResuming += () => Paused = false; + + // Stop when the app is being shut down. (Orignally it used IApplication from Sentry but it's internal) + OnQuittingCleanUpCandidate oqcuc = new (nameof(DclAnrWatchDog), StopNoWait); + ExitUtils.RegisterCleanUpCandidate(oqcuc); + } + + internal void StopNoWait() + { + this.Stop(wait: false); + } + + internal abstract void Stop(bool wait = false); + + private static (string message, string? filePath) NewDumpAttachment() + { +#if UNITY_STANDALONE_WIN + Result<(string filePath, string zipPath)> dumpResult = ThreadsDumpUtility.CollectAndArchiveDumpInfoToAppDir(); + if (dumpResult.Success == false) + { + return ($"Dump cannot be collected: {dumpResult.ErrorMessage}", null); + } + + return ("Dump collected", dumpResult.Value.zipPath); +#else + return ("Dump is not available on macOS yet", null); +#endif + } + + protected void Report() + { + // Don't report events while in the background. + if (!Paused) + { + (string dumpMessage, string? filePath) = NewDumpAttachment(); + + System.Text.StringBuilder sb = new (); + sb.Append("DclApplication not responding for at least "); + sb.Append(DetectionTimeoutMs); + sb.Append(" ms. "); + sb.Append(dumpMessage); + string message = sb.ToString(); + + Logger?.LogInfo("Detected an DclAnr event: {0}", message); + +#if UNITY_STANDALONE_WIN + var exception = new DclApplicationNotRespondingException(message, filePath); +#else + var exception = new DclApplicationNotRespondingException(message); +#endif + + exception.SetSentryMechanism(Mechanism, "Main thread unresponsive.", false); + OnApplicationNotResponding?.Invoke(this, exception); + } + } + } + + internal class DclAnrWatchDogMultiThreaded : DclAnrWatchDog + { + private int _ticksSinceUiUpdate; // how many _sleepIntervalMs have elapsed since the UI updated last time + private bool _reported; // don't report the same ANR instance multiple times + private bool _stop; + private readonly Thread _thread = null!; + + internal DclAnrWatchDogMultiThreaded(IDiagnosticLogger? logger, SentryMonoBehaviour monoBehaviour, TimeSpan detectionTimeout) + : base(logger, monoBehaviour, detectionTimeout) + { + _thread = new Thread(Run) + { + Name = "Sentry-DclAnr-WatchDog", + IsBackground = true, // do not block on app shutdown + Priority = System.Threading.ThreadPriority.BelowNormal, + }; + _thread.Start(); + + // Update the UI status periodically by running a coroutine on the UI thread + MonoBehaviour.StartCoroutine(UpdateUiStatus()); + } + + internal override void Stop(bool wait = false) + { + _stop = true; + if (wait) + { + _thread.Join(); + } + } + + private IEnumerator UpdateUiStatus() + { + var waitForSeconds = new UnityEngine.WaitForSecondsRealtime((float)SleepIntervalMs / 1000); + + yield return waitForSeconds; + while (!_stop) + { + Interlocked.Exchange(ref _ticksSinceUiUpdate, 0); + _reported = false; + yield return waitForSeconds; + } + } + + private void Run() + { + try + { + var reportThreshold = DetectionTimeoutMs / SleepIntervalMs; + + Logger?.Log(SentryLevel.Info, + "Starting an DclAnr WatchDog - detection timeout: {0} ms, check every {1} ms => report after {2} failed checks", + null, DetectionTimeoutMs, SleepIntervalMs, reportThreshold); + + while (!_stop) + { + Interlocked.Increment(ref _ticksSinceUiUpdate); + Thread.Sleep(SleepIntervalMs); + + if (Paused) + { + Interlocked.Exchange(ref _ticksSinceUiUpdate, 0); + } + else if (_ticksSinceUiUpdate >= reportThreshold && !_reported) + { + Report(); + _reported = true; + } + } + } + catch (ThreadAbortException e) + { + Logger?.Log(SentryLevel.Debug, "DclAnr watchdog thread aborted.", e); + } + catch (Exception e) + { + Logger?.Log(SentryLevel.Error, "Exception in the DclAnr watchdog.", e); + } + } + } + + internal class DclAnrWatchDogSingleThreaded : DclAnrWatchDog + { + private readonly Stopwatch _watch = new(); + private bool _stop; + + private UnityEngine.Coroutine? _updateUiStatusCoroutine; + + internal DclAnrWatchDogSingleThreaded(IDiagnosticLogger? logger, SentryMonoBehaviour monoBehaviour, TimeSpan detectionTimeout) + : base(logger, monoBehaviour, detectionTimeout) + { + Logger?.LogInfo("Starting an DclAnr Watchdog - Detection timeout: {0} ms, check every {1} ms", DetectionTimeoutMs, SleepIntervalMs); + + // Check the UI status periodically by running a coroutine on the UI thread and checking the elapsed time + _watch.Start(); + _updateUiStatusCoroutine = MonoBehaviour.StartCoroutine(UpdateUiStatus()); + + // We're stuck on the main thread, and we're using timestamps: We have to reset the coroutine when the app + // loses and regains focus to avoid reporting false positives. + MonoBehaviour.ApplicationPausing += () => + { + logger?.LogDebug("Stopping DclAnr detection coroutine."); + _watch.Stop(); + + MonoBehaviour.StopCoroutine(_updateUiStatusCoroutine); + _updateUiStatusCoroutine = null; + }; + MonoBehaviour.ApplicationResuming += () => + { + logger?.LogDebug("Restarting DclAnr detection coroutine."); + + _watch.Restart(); + if (_updateUiStatusCoroutine is null) + { + _updateUiStatusCoroutine = MonoBehaviour.StartCoroutine(UpdateUiStatus()); + } + else + { + logger?.LogError("Attempted to restart the DclAnr detection but it was not stopped."); + } + }; + } + + internal override void Stop(bool wait = false) + { + _stop = true; + if (_updateUiStatusCoroutine != null) + { + MonoBehaviour.StopCoroutine(_updateUiStatusCoroutine); + _updateUiStatusCoroutine = null; + } + } + + private IEnumerator UpdateUiStatus() + { + _watch.Start(); + var waitForSeconds = new UnityEngine.WaitForSecondsRealtime((float)SleepIntervalMs / 1000); + while (!_stop) + { + if (_watch.ElapsedMilliseconds >= DetectionTimeoutMs) + { + Report(); + } + _watch.Restart(); + yield return waitForSeconds; + } + } + } + +// Not supported on macOS yet +#if UNITY_STANDALONE_WIN + +#if UNITY_EDITOR + [UnityEditor.InitializeOnLoad] +#endif + public static class ThreadsDumpUtility + { + private static string APP_PATH; // Cache, because Unity API is not available from none-main thread + private static string STREAMING_PATH; // Cache, because Unity API is not available from none-main thread + +#if UNITY_EDITOR + static ThreadsDumpUtility() + { + Init(); + } +#endif + + [UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)] + private static void Init() + { + APP_PATH = UnityEngine.Application.persistentDataPath; // Cache, because Unity API is not available from none-main thread + STREAMING_PATH = UnityEngine.Application.streamingAssetsPath; // Cache, because Unity API is not available from none-main thread + } + + public static Result CollectDumpInfoFile(string targetDmpPath) + { + Result result = MiniDumpNative.CollectSelfMiniDump(targetDmpPath); + if (result.Success == false) + { + return Result.ErrorResult($"CollectSelfMiniDump error: {result.ErrorMessage}"); + } + + Result waitResult = WaitUntilDumpReady(targetDmpPath); + if (waitResult.Success == false) + { + return Result.ErrorResult($"Target file is not written: {waitResult.ErrorMessage}"); + } + + return Result.SuccessResult(); + } + + public static Result WaitUntilDumpReady(string targetDmpPath) + { + const int TIMEOUT_MS = 5_000; + const int POLL_MS = 100; + + Stopwatch sw = Stopwatch.StartNew(); + + while (sw.ElapsedMilliseconds < TIMEOUT_MS) + { + if (File.Exists(targetDmpPath)) + { + try + { + using FileStream stream = new FileStream( + targetDmpPath, + FileMode.Open, + FileAccess.Read, + FileShare.None + ); + + if (stream.Length > 0) + return Result.SuccessResult(); + } + catch (IOException) + { + // still being written + } + catch (UnauthorizedAccessException) + { + // transient access state + } + } + + Thread.Sleep(POLL_MS); + } + + return Result.ErrorResult( + $"Timed out waiting for dump file: {targetDmpPath}" + ); + } + + public static string NewDumpFilePath() + { + string fileName = System.IO.Path.GetRandomFileName(); + fileName = System.IO.Path.ChangeExtension(fileName, ".dmp"); + string filePath = System.IO.Path.Combine(APP_PATH, fileName); + return filePath; + } + + // Returns zip path + public static Result ArchiveIntoZip(string filePath) + { + bool exists = System.IO.File.Exists(filePath); + + if (exists == false) + { + return Result.ErrorResult("Original file does not exist"); + } + + string zipPath = System.IO.Path.ChangeExtension(filePath, ".zip"); + string fileName = System.IO.Path.GetFileName(filePath); + + using ZipArchive zip = ZipFile.Open(zipPath, ZipArchiveMode.Create); + zip.CreateEntryFromFile(filePath, fileName); + + return Result.SuccessResult(zipPath); + } + + public static Result CollectDumpInfoBase64() + { + Result<(string filePath, string zipPath)> result = CollectAndArchiveDumpInfoToAppDir(); + if (result.Success == false) + { + return Result.ErrorResult($"Error on Dump Current: {result.ErrorMessage}"); + } + + byte[] bytes = System.IO.File.ReadAllBytes(result.Value.zipPath); // yes, it allocs but rarely called + string base64String = System.Convert.ToBase64String(bytes); + + // clean the temp files + System.IO.File.Delete(result.Value.filePath); + System.IO.File.Delete(result.Value.zipPath); + + return Result.SuccessResult(base64String); + } + + +#if UNITY_EDITOR + [UnityEditor.MenuItem("Tools/ProcDump/Dump Current")] + public static void DumpCurrent() + { + Result<(string filePath, string zipPath)> result = CollectAndArchiveDumpInfoToAppDir(); + if (result.Success == false) + { + Debug.LogError($"Error on Dump Current: {result.ErrorMessage}"); + return; + } + + Debug.Log($"Successfully dumped and archive at: {result.Value.filePath}, {result.Value.zipPath}"); + } +#endif + + public static Result<(string filePath, string zipPath)> CollectAndArchiveDumpInfoToAppDir() + { + string filePath = NewDumpFilePath(); + Result result = CollectDumpInfoFile(filePath); + if (result.Success == false) + { + return Result<(string filePath, string zipPath)>.ErrorResult($"Error on dumping: {result.ErrorMessage}"); + } + + Result zipPathResult = ArchiveIntoZip(filePath); + if (zipPathResult.Success == false) + { + return Result<(string filePath, string zipPath)>.ErrorResult($"Error on archiving: {zipPathResult.ErrorMessage}"); + } + + string zipPath = zipPathResult.Value; + + return Result<(string filePath, string zipPath)>.SuccessResult((filePath, zipPath)); + } + + } + + internal static class ProcessInfoNative + { + private const uint PROCESS_QUERY_INFORMATION = 0x0400; + private const uint PROCESS_VM_READ = 0x0010; + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr OpenProcess( + uint dwDesiredAccess, + bool bInheritHandle, + uint dwProcessId); + + [DllImport("kernel32.dll")] + private static extern bool CloseHandle(IntPtr hObject); + + public static ProcessHandle OpenSelf(UInt32 pid) + { + IntPtr handle = OpenProcess( + PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, + false, + pid); + + if (handle == IntPtr.Zero) throw new Win32Exception(Marshal.GetLastWin32Error()); // this is a really exceptional case + return new ProcessHandle(handle); + } + + public static void Close(IntPtr handle) + { + if (handle != IntPtr.Zero) + CloseHandle(handle); + } + + } + + public readonly struct ProcessHandle : IDisposable + { + public readonly IntPtr handle; + + public ProcessHandle(IntPtr handle) + { + this.handle = handle; + } + + public void Dispose() + { + ProcessInfoNative.Close(handle); + } + } + + internal static class MiniDumpNative + { + // from https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/ne-minidumpapiset-minidump_type + [Flags] + private enum MINIDUMP_TYPE : uint { + MiniDumpNormal = 0x00000000, + MiniDumpWithDataSegs = 0x00000001, + MiniDumpWithFullMemory = 0x00000002, + MiniDumpWithHandleData = 0x00000004, + MiniDumpFilterMemory = 0x00000008, + MiniDumpScanMemory = 0x00000010, + MiniDumpWithUnloadedModules = 0x00000020, + MiniDumpWithIndirectlyReferencedMemory = 0x00000040, + MiniDumpFilterModulePaths = 0x00000080, + MiniDumpWithProcessThreadData = 0x00000100, + MiniDumpWithPrivateReadWriteMemory = 0x00000200, + MiniDumpWithoutOptionalData = 0x00000400, + MiniDumpWithFullMemoryInfo = 0x00000800, + MiniDumpWithThreadInfo = 0x00001000, + MiniDumpWithCodeSegs = 0x00002000, + MiniDumpWithoutAuxiliaryState = 0x00004000, + MiniDumpWithFullAuxiliaryState = 0x00008000, + MiniDumpWithPrivateWriteCopyMemory = 0x00010000, + MiniDumpIgnoreInaccessibleMemory = 0x00020000, + MiniDumpWithTokenInformation = 0x00040000, + MiniDumpWithModuleHeaders = 0x00080000, + MiniDumpFilterTriage = 0x00100000, + MiniDumpWithAvxXStateContext = 0x00200000, + MiniDumpWithIptTrace = 0x00400000, + MiniDumpScanInaccessiblePartialPages = 0x00800000, + MiniDumpFilterWriteCombinedMemory, + MiniDumpValidTypeFlags = 0x01ffffff, + // MiniDumpNoIgnoreInaccessibleMemory, + // MiniDumpValidTypeFlagsEx + } + + [DllImport("Dbghelp.dll", SetLastError = true)] + private static extern bool MiniDumpWriteDump( + IntPtr hProcess, + UInt32 processId, + IntPtr hFile, + MINIDUMP_TYPE dumpType, + IntPtr exceptionParam, + IntPtr userStreamParam, + IntPtr callbackParam + ); + + // It's approx the -mt flag + private const MINIDUMP_TYPE dumpType = ( + MINIDUMP_TYPE.MiniDumpNormal | + MINIDUMP_TYPE.MiniDumpWithThreadInfo | + MINIDUMP_TYPE.MiniDumpWithHandleData | + MINIDUMP_TYPE.MiniDumpWithUnloadedModules + ); + + public static Result CollectSelfMiniDump(string targetDmpPath) + { + using FileStream targetFile = File.Open(targetDmpPath, FileMode.Create, FileAccess.Write, FileShare.None); + IntPtr hFile = targetFile.SafeFileHandle.DangerousGetHandle(); + + UInt32 pid = (UInt32) Process.GetCurrentProcess().Id; // IL2CPP safe + using ProcessHandle hProcess = ProcessInfoNative.OpenSelf(pid); + + bool ok = MiniDumpWriteDump( + hProcess.handle, + pid, + hFile, + dumpType, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero + ); + + if (ok == false) + { + return Result.ErrorResult(new Win32Exception(Marshal.GetLastWin32Error()).Message); + } + + return Result.SuccessResult(); + } + } + +#endif + +} + +#endif diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrIntegration.cs.meta b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrIntegration.cs.meta new file mode 100644 index 00000000000..d17ce76b236 --- /dev/null +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrIntegration.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4575e653e960e40fbb6274d5976900a5 \ No newline at end of file diff --git a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/SentryBuildTimeConfiguration.cs b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/SentryBuildTimeConfiguration.cs index ef52d5c295f..8682d0ce6bb 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/SentryBuildTimeConfiguration.cs +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/SentryBuildTimeConfiguration.cs @@ -24,6 +24,13 @@ public override void Configure(SentryUnityOptions options) { options.SetBeforeSend(AddUnspecifiedCategory); + // Implements custom ANR tracing with minidumps and callstack collection + SentryMonoBehaviour monoInstance = SentryMonoBehaviour.Instance; + DclAnrIntegration anrIntegration = new DclAnrIntegration(monoInstance); + options.AddIntegration(anrIntegration); + + options.DisableAnrIntegration(); + #if UNITY_EDITOR bool isDirty = false; @@ -167,6 +174,7 @@ private void PersistIntoAssetFile(string path, SentryUnityOptions options) { ScriptableSentryUnityOptions asset = AssetDatabase.LoadAssetAtPath(path); if (asset == null) return; + asset.Enabled = options.Enabled; asset.ReleaseOverride = options.Release; asset.Dsn = options.Dsn; asset.EnvironmentOverride = options.Environment; diff --git a/Explorer/Assets/Plugins/DclNativeProcesses/DCLProcesses.dll b/Explorer/Assets/Plugins/DclNativeProcesses/DCLProcesses.dll index bde7d08cfdb..f031f84fc20 100644 --- a/Explorer/Assets/Plugins/DclNativeProcesses/DCLProcesses.dll +++ b/Explorer/Assets/Plugins/DclNativeProcesses/DCLProcesses.dll @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbd574f6261458e78f42eadd3470e577ebd397ba900e5711fc9c1848b8c14d43 +oid sha256:08f814f5e03afcf93544135f229ce663c0cde6fe43367fa889856717a3109042 size 117248 diff --git a/Explorer/Assets/Plugins/DclNativeProcesses/DCLProcesses.exp b/Explorer/Assets/Plugins/DclNativeProcesses/DCLProcesses.exp index 3d67f3fbb70..62b1927f53b 100644 Binary files a/Explorer/Assets/Plugins/DclNativeProcesses/DCLProcesses.exp and b/Explorer/Assets/Plugins/DclNativeProcesses/DCLProcesses.exp differ diff --git a/Explorer/Assets/Plugins/DclNativeProcesses/DCLProcesses.lib b/Explorer/Assets/Plugins/DclNativeProcesses/DCLProcesses.lib index d3b69726932..4f5cb0b7aeb 100644 Binary files a/Explorer/Assets/Plugins/DclNativeProcesses/DCLProcesses.lib and b/Explorer/Assets/Plugins/DclNativeProcesses/DCLProcesses.lib differ diff --git a/Explorer/Assets/Plugins/DclNativeProcesses/DclProcessesNativeMethods.cs b/Explorer/Assets/Plugins/DclNativeProcesses/DclProcessesNativeMethods.cs index 7abee1ab178..bea293bc0e8 100644 --- a/Explorer/Assets/Plugins/DclNativeProcesses/DclProcessesNativeMethods.cs +++ b/Explorer/Assets/Plugins/DclNativeProcesses/DclProcessesNativeMethods.cs @@ -29,6 +29,14 @@ public static extern int start_process( string[] args, int argc ); + + [DllImport(LIB_NAME, CallingConvention = CallingConvention.Cdecl)] + public static extern int dcl_start_process_blocking( + string fileName, + [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPUTF8Str, SizeParamIndex = 1)] + string[] args, + int argc + ); } public static class DclProcesses @@ -43,6 +51,11 @@ public static Result Start(string fileName, string[] args) int pid = resultCode; return Result.SuccessResult(pid); } + + public static int ExecuteBlocking(string fileName, string[] args) + { + return DclProcessesNativeMethods.dcl_start_process_blocking(fileName, args, args.Length); + } } public readonly struct ProcessName : IDisposable diff --git a/Explorer/Assets/Plugins/DclNativeProcesses/dcl_processes.c b/Explorer/Assets/Plugins/DclNativeProcesses/dcl_processes.c index 7da33413934..e82348c455b 100644 --- a/Explorer/Assets/Plugins/DclNativeProcesses/dcl_processes.c +++ b/Explorer/Assets/Plugins/DclNativeProcesses/dcl_processes.c @@ -15,10 +15,11 @@ #ifdef __APPLE__ #include #include +#include #include #include #include -#include +#include #include #include @@ -36,13 +37,13 @@ char* get_process_name(pid_t pid) { char* buffer = malloc(sizeof(char) * MAX_PATH); // Get the process name if (GetModuleBaseNameA(hProcess, NULL, buffer, MAX_PATH)) { + CloseHandle(hProcess); return buffer; } else { + CloseHandle(hProcess); free(buffer); } - - CloseHandle(hProcess); } #endif @@ -127,8 +128,8 @@ int start_process(char* filename, char** args, int argc) { // posix_spawnp searches PATH, it inherits current env and file actions/attrs. int rc = posix_spawnp( - &pid, - filename, + &pid, + filename, NULL, // file_actions NULL, // attrp argv, environ @@ -140,5 +141,55 @@ int start_process(char* filename, char** args, int argc) { return -1; } return (int)pid; -#endif +#endif +} + +int dcl_start_process_blocking(char* filename, char** args, int argc) { + char** argv = make_argv_internal(filename, args, argc); + if (!argv) return -1; + +#ifdef _WIN32 + // _P_WAIT blocks until the child exits and returns its exit code (-1 on spawn failure). + intptr_t rc = _spawnvp(_P_WAIT, filename, (const char* const*)argv); + int saved_errno = errno; + free(argv); + + if (rc == -1) { + errno = saved_errno; + return -1; + } + return (int)rc; +#endif + +#ifdef __APPLE__ + pid_t pid = -1; + + int rc = posix_spawnp( + &pid, + filename, + NULL, // file_actions + NULL, // attrp + argv, environ + ); + free(argv); + + if (rc != 0) { + errno = rc; + return -1; + } + + int status = 0; + while (waitpid(pid, &status, 0) == -1) { + if (errno != EINTR) { + return -1; + } + } + + if (WIFEXITED(status)) { + return WEXITSTATUS(status); + } + + // killed by signal or otherwise abnormal termination + return -1; +#endif } diff --git a/Explorer/Assets/Plugins/DclNativeProcesses/dcl_processes.h b/Explorer/Assets/Plugins/DclNativeProcesses/dcl_processes.h index 5e2a79d3b03..786f69a546d 100644 --- a/Explorer/Assets/Plugins/DclNativeProcesses/dcl_processes.h +++ b/Explorer/Assets/Plugins/DclNativeProcesses/dcl_processes.h @@ -21,6 +21,10 @@ EXPORT char* get_process_name(pid_t pid); EXPORT void free_name(char* name); +// returns pid or -1 EXPORT int start_process(char* filename, char** args, int argc); +// return exit code of the process +EXPORT int dcl_start_process_blocking(char* filename, char** args, int argc); + #endif