From aca6f8bf55294f73f24b059e1868a16a937f74b4 Mon Sep 17 00:00:00 2001 From: NickKhalow Date: Fri, 22 May 2026 18:38:53 +0400 Subject: [PATCH] feat: ANR detection with minidump collection Integrate custom ANR detector with Sentry reporting, native Windows minidump collection, background archival/upload, debug chat commands, and CI flags for opt-in builds. --- .github/workflows/build-unitycloud.yml | 175 ++++- .../DCL/Chat/Commands/AnrDumpChatCommand.cs | 29 + .../Chat/Commands/AnrDumpChatCommand.cs.meta | 2 + .../Chat/Commands/AnrSimulateChatCommand.cs | 32 + .../Commands/AnrSimulateChatCommand.cs.meta | 2 + .../CommandsHandleChatMessageBus.cs | 7 +- .../AppArgs/ApplicationParametersParser.cs | 3 + .../Global/Dynamic/DynamicWorldContainer.cs | 4 + .../DCL/Infrastructure/Utility/Utility.asmdef | 6 +- .../Diagnostics/DiagnosticInfoUtils.cs | 10 + .../ReportsHandling/Sentry/DclAnrException.cs | 18 + .../Sentry/DclAnrException.cs.meta | 2 + .../Sentry/DclAnrIntegration.cs | 604 ++++++++++++++++++ .../Sentry/DclAnrIntegration.cs.meta | 2 + .../Sentry/SentryBuildTimeConfiguration.cs | 8 + .../DclNativeProcesses/DCLProcesses.dll | 2 +- .../DclNativeProcesses/DCLProcesses.exp | Bin 974 -> 1130 bytes .../DclNativeProcesses/DCLProcesses.lib | Bin 2252 -> 2512 bytes .../DclProcessesNativeMethods.cs | 13 + .../DclNativeProcesses/dcl_processes.c | 63 +- .../DclNativeProcesses/dcl_processes.h | 4 + 21 files changed, 965 insertions(+), 21 deletions(-) create mode 100644 Explorer/Assets/DCL/Chat/Commands/AnrDumpChatCommand.cs create mode 100644 Explorer/Assets/DCL/Chat/Commands/AnrDumpChatCommand.cs.meta create mode 100644 Explorer/Assets/DCL/Chat/Commands/AnrSimulateChatCommand.cs create mode 100644 Explorer/Assets/DCL/Chat/Commands/AnrSimulateChatCommand.cs.meta create mode 100644 Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrException.cs create mode 100644 Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrException.cs.meta create mode 100644 Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrIntegration.cs create mode 100644 Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrIntegration.cs.meta diff --git a/.github/workflows/build-unitycloud.yml b/.github/workflows/build-unitycloud.yml index fe0613771b7..33e9d7edd82 100644 --- a/.github/workflows/build-unitycloud.yml +++ b/.github/workflows/build-unitycloud.yml @@ -8,6 +8,7 @@ on: - synchronize - ready_for_review - labeled + - unlabeled merge_group: {} push: branches: @@ -52,6 +53,11 @@ on: required: false type: boolean default: false + script_debugging: + description: 'Enable Script Debugging (forces Development build)' + required: false + type: boolean + default: false delta_threshold_macos_mb: description: 'Warn if macOS growth > this MB (0 = off)' required: false @@ -77,6 +83,15 @@ on: required: false default: false type: boolean + platforms: + description: 'Which platforms to build' + required: true + default: 'both' + type: choice + options: + - both + - windows-only + - macos-only workflow_call: inputs: profile: @@ -102,6 +117,10 @@ on: required: false type: boolean default: false + script_debugging: + required: false + type: boolean + default: false is_release_build: type: boolean required: false @@ -152,9 +171,11 @@ concurrency: cancel-in-progress: >- ${{ github.event_name != 'pull_request' || - github.event.action != 'labeled' || + (github.event.action != 'labeled' && github.event.action != 'unlabeled') || github.event.label.name == 'force-build' || - github.event.label.name == 'clean-build' + github.event.label.name == 'clean-build' || + github.event.label.name == 'windows-only' || + github.event.label.name == 'macos-only' }} jobs: @@ -183,8 +204,13 @@ jobs: ) || github.event.action == 'ready_for_review' || ( - github.event.action == 'labeled' && - (github.event.label.name == 'force-build' || github.event.label.name == 'clean-build') + (github.event.action == 'labeled' || github.event.action == 'unlabeled') && + ( + github.event.label.name == 'force-build' || + github.event.label.name == 'clean-build' || + github.event.label.name == 'windows-only' || + github.event.label.name == 'macos-only' + ) ) ) ) @@ -200,6 +226,7 @@ jobs: clean_build: ${{ steps.set_defaults.outputs.clean_build }} cache_strategy: ${{ steps.set_defaults.outputs.cache_strategy }} install_source: ${{ steps.set_defaults.outputs.install_source }} + targets: ${{ steps.get_targets.outputs.targets }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -318,13 +345,20 @@ jobs: run: | #!/bin/bash - if [[ "${{ github.event.inputs.sentry_enabled || inputs.sentry_enabled }}" == "true" ]]; then + sentry_enabled="${{ github.event.inputs.sentry_enabled || inputs.sentry_enabled }}" + + if [[ "$sentry_enabled" != "true" && "${{ github.event_name }}" == "pull_request" ]]; then + echo "Checking PR labels: ${{ join(github.event.pull_request.labels.*.name, ', ') }}" + sentry_enabled=$(echo "${{ join(github.event.pull_request.labels.*.name, ' ') }}" | grep -qw 'enable-sentry' && echo true || echo false) + fi + + if [[ "$sentry_enabled" == "true" ]]; then if [[ "${{ inputs.is_release_build }}" == "true" ]]; then echo "environment=production" >> "$GITHUB_OUTPUT" else echo "environment=development" >> "$GITHUB_OUTPUT" fi - + echo "upload_symbols=true" >> "$GITHUB_OUTPUT" echo "sentry_enabled=true" >> "$GITHUB_OUTPUT" else @@ -402,11 +436,76 @@ jobs: options+=("EnableDeepProfilingSupport") fi + # Script Debugging: input toggle (workflow_dispatch / workflow_call) or 'script-debugging' PR label. + # AllowDebugging requires a Development build to be honored by Unity. + script_debugging="${{ github.event.inputs.script_debugging || inputs.script_debugging }}" + + if [[ "$script_debugging" != "true" && "${{ github.event_name }}" == "pull_request" ]]; then + script_debugging=$(echo "${{ join(github.event.pull_request.labels.*.name, ' ') }}" | grep -qw 'script-debugging' && echo true || echo false) + fi + + if [[ "$script_debugging" == "true" ]]; then + # Add Development only if not already added by profile mode + if [[ ! " ${options[*]} " =~ " Development " ]]; then + options+=("Development") + fi + options+=("AllowDebugging") + fi + # Write the array as a comma-separated string # Set the Internal Field Separator to comma IFS=, echo "options=${options[*]}" | tee -a "$GITHUB_OUTPUT" + - name: Determine build targets + if: steps.decide.outputs.should_build == 'true' + id: get_targets + shell: bash + run: | + set -euo pipefail + + # platform_build_mode: windows | macos | both + platform_build_mode=both + + dispatch_platforms="${{ github.event.inputs.platforms }}" + echo "Dispatch platforms: '$dispatch_platforms'" + + if [ "$dispatch_platforms" = "windows-only" ]; then + platform_build_mode=windows + elif [ "$dispatch_platforms" = "macos-only" ]; then + platform_build_mode=macos + else + labels="${{ join(github.event.pull_request.labels.*.name, ' ') }}" + echo "Labels: '$labels'" + + label_match_count=0 + if echo "$labels" | grep -qw 'windows-only'; then + platform_build_mode=windows + label_match_count=$((label_match_count + 1)) + fi + if echo "$labels" | grep -qw 'macos-only'; then + platform_build_mode=macos + label_match_count=$((label_match_count + 1)) + fi + + if [ "$label_match_count" -gt 1 ]; then + echo "::error::Both 'windows-only' and 'macos-only' labels are applied. Remove one — they are mutually exclusive." + exit 1 + fi + fi + + echo "Platform build mode: $platform_build_mode" + + case "$platform_build_mode" in + windows) targets='["windows64"]' ;; + macos) targets='["macos"]' ;; + both) targets='["windows64","macos"]' ;; + *) echo "::error::Unknown platform_build_mode '$platform_build_mode'"; exit 1 ;; + esac + + echo "Targets: $targets" + echo "targets=$targets" >> "$GITHUB_OUTPUT" + build: name: Build runs-on: ubuntu-latest @@ -416,7 +515,7 @@ jobs: strategy: fail-fast: false matrix: - target: ['windows64', 'macos'] + target: ${{ fromJSON(needs.prebuild.outputs.targets) }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -724,21 +823,36 @@ jobs: *) echo "BUILD_PREFIX=gn" >> $GITHUB_ENV ;; esac + - name: Compute S3 destination path + env: + RESOLVED_DESTINATION_PATH: "${{ + inputs.is_release_build && format('@dcl/{0}/releases/{1}', github.event.repository.name, inputs.tag_version) + || format('@dcl/{0}/branch/{1}/{2}-{3}-{4}', github.event.repository.name, env.SAFE_BRANCH_NAME, env.BUILD_PREFIX, github.run_number, env.SHA_SHORT) + }}" + run: | + echo "DESTINATION_PATH=${RESOLVED_DESTINATION_PATH}" >> $GITHUB_ENV + - name: Upload artifact to S3 env: AWS_ACCESS_KEY_ID: ${{ secrets.EXPLORER_TEAM_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.EXPLORER_TEAM_AWS_SECRET_ACCESS_KEY }} EXPLORER_TEAM_S3_BUCKET: ${{ secrets.EXPLORER_TEAM_S3_BUCKET }} - DESTINATION_PATH: "${{ - inputs.is_release_build && format('@dcl/{0}/releases/{1}', github.event.repository.name, inputs.tag_version) - || format('@dcl/{0}/branch/{1}/{2}-{3}-{4}', github.event.repository.name, env.SAFE_BRANCH_NAME, env.BUILD_PREFIX, github.run_number, env.SHA_SHORT) - }}" run: | npx @dcl/cdn-uploader@next \ --bucket $EXPLORER_TEAM_S3_BUCKET \ --local-folder upload_to_s3 \ --bucket-folder $DESTINATION_PATH + - name: Print S3 upload URL + run: | + ARTIFACT_URL="https://explorer-artifacts.decentraland.org/${DESTINATION_PATH}/${{ env.artifact_name }}.zip" + echo "::notice::Artifact uploaded to: ${ARTIFACT_URL}" + { + echo "### Artifact upload URL (${{ matrix.target }})" + echo + echo "[${ARTIFACT_URL}](${ARTIFACT_URL})" + } >> "$GITHUB_STEP_SUMMARY" + - name: Upload debug symbols uses: actions/upload-artifact@v4 with: @@ -801,4 +915,43 @@ jobs: ORG_ID: ${{ secrets.UNITY_CLOUD_ORG_ID }} PROJECT_ID: ${{ secrets.UNITY_CLOUD_PROJECT_ID }} run: python -u scripts/cloudbuild/build.py --cancel + + build-gate: + name: Build Gate (Windows + macOS) + runs-on: ubuntu-latest + needs: [prebuild, build] + if: always() && github.event_name == 'pull_request' + steps: + - name: Verify both targets built and passed + shell: bash + run: | + set -euo pipefail + + should_build='${{ needs.prebuild.outputs.should_build }}' + prebuild_result='${{ needs.prebuild.result }}' + + # No Explorer/ changes, draft without override, or perf_test path — gate is not applicable. + if [ "$prebuild_result" != "success" ] || [ "$should_build" != "true" ]; then + echo "Gate not applicable (prebuild=$prebuild_result, should_build='$should_build'). Passing." + exit 0 + fi + + targets='${{ needs.prebuild.outputs.targets }}' + echo "Targets that ran: $targets" + + has_windows=$(echo "$targets" | jq 'any(. == "windows64")') + has_macos=$(echo "$targets" | jq 'any(. == "macos")') + if [ "$has_windows" != "true" ] || [ "$has_macos" != "true" ]; then + echo "::error::Build Gate requires both Windows and macOS builds. This run was filtered to: $targets" + echo "::error::Remove the 'windows-only' or 'macos-only' label so both targets build, then push or re-trigger." + exit 1 + fi + + build_result='${{ needs.build.result }}' + if [ "$build_result" != "success" ]; then + echo "::error::Build matrix did not succeed (result: $build_result)" + exit 1 + fi + + echo "Both Windows and macOS builds passed." # Test change 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 feaa2c4368f..e6d85e08e57 100644 --- a/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs +++ b/Explorer/Assets/DCL/Infrastructure/Global/Dynamic/DynamicWorldContainer.cs @@ -583,6 +583,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/DiagnosticInfoUtils.cs b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/DiagnosticInfoUtils.cs index d47ca505cac..f7e48dbb85a 100644 --- a/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/DiagnosticInfoUtils.cs +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/DiagnosticInfoUtils.cs @@ -60,6 +60,16 @@ public static void LogSystem(string version) stringBuilder.AppendFormat("Window Mode: {0}\n", Screen.fullScreenMode.ToString()); AppendFooter(stringBuilder); + AppendHeader(stringBuilder, "SENTRY"); + stringBuilder.AppendFormat("Enabled: {0}\n", SentrySdk.IsEnabled); + SentryUnityOptions? sentryOptions = ScriptableSentryUnityOptions.LoadSentryUnityOptions(); + if (sentryOptions != null) + { + stringBuilder.AppendFormat("Environment: {0}\n", sentryOptions.Environment ?? ""); + stringBuilder.AppendFormat("Release: {0}\n", sentryOptions.Release ?? ""); + stringBuilder.AppendFormat("DSN: {0}\n", string.IsNullOrEmpty(sentryOptions.Dsn) ? "" : ""); + } + AppendFooter(stringBuilder); ReportHub.LogProductionInfo(stringBuilder.ToString()); } 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..9d8d6b17c1a --- /dev/null +++ b/Explorer/Assets/DCL/PerformanceAndDiagnostics/Diagnostics/ReportsHandling/Sentry/DclAnrIntegration.cs @@ -0,0 +1,604 @@ +// 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 DCL.Utility; +using RichTypes; +using System.IO; +using System.IO.Compression; +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 3d67f3fbb700e39a6f3426ea54775e20b52ba1e3..62b1927f53b7f2bc30f13076140131466c4f54ef 100644 GIT binary patch delta 530 zcmX@d{)&S)rHzToU5KmrJOfYL1G}# z0F-Z<7^u$50_3nvY*d{%fya=Mfr){c!Nu7ppeR2%wYWI7ST7|fhan|7C%(8Ov8W`z z03sfrl#`#Fotc+DahKM_$GV{)OPCoffFuW)r2`~+!7Ljf$q8l|07*VD%K=DofmtR% zk{`@+0g~Kc7RWsxfw*LIAfqs2up)znAjm7Y-M}CKRKf&Ql2(+O8lRV#o5~;#6afYZ zLwYL0$zTZ)paje0DNKos0h7NmDc6IX3^JXWp(wrFzX%v`9&iyAKLa2zWJp94F=D7e y6ES9JLlZG!m<|^KG8jOff&q|I85rb2gg1x)0*H_%go7kvg(3oS8tz~KMg;)n5nML_ delta 330 zcmaFGagLogrHzTn>%7zDV0*a3+2Qc{yj(^Y~&VjvL9 z$iUz+F;JbA8OULt*r+MO$iT$l;_MSpl%JehT%200my(k+*?~oCG8dzcBR5DDg9(u2 z0JC&}BoCNn0VFxWECV3P3uf5>Nsz_N3@kwMBM=8}?q(EbG!S605CmGxkXDqM8lRV# zo5~;#6al)OAw9Juz5rr1L_!28!94jRQzE0=E-@KKtDRbMO6F@ kfWVL;98JWCArVc)n4xI$MP_+MzRCBQ<(YLE7(hM*01>h{1ONa4 diff --git a/Explorer/Assets/Plugins/DclNativeProcesses/DCLProcesses.lib b/Explorer/Assets/Plugins/DclNativeProcesses/DCLProcesses.lib index d3b6972693265a20794921983981430fcc6b7d4f..4f5cb0b7aebb25a3353838ded3027a4bf859a250 100644 GIT binary patch delta 367 zcmX>jctLoAv#7Zl|urxq8-C*|ZPXJ_W6GvJZZSD5^OMbFmQz)S%G61dob zZqQ(6U`S(SU|0mS+d4 zZSY8M=3-jHYGq~uwO=8Di-F<)e+Gt>w$2~%S9O4vF#xT>W4srK)8uvRBAgMo@4l7> kxoz@k_GHdArEK?=fIOzj<{X)vQMyGx<$*lr$tyXO0aDdvZ~y=R delta 193 zcmca0d`57BrLmEP0t6&*F)%Q2GB7YLU|?Xm$H2fQ#lXP+3X1E1G!S#dfbe8K)`*E` zQke7=CV$}2Gc__s7{LZKV*wKb!#x%T1}Pxt6*~h%9T3NGfX!xLWME=oWnf`on|zbe zbFwp&DN9;WYU;$h;+xkob+As}#390Y-1)#GX`nF-lh1P`bC%9#J+1`gFiy7S%;cQ0 P 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