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 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