Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Runtime/Native/OSX/NativeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ public NativeClient(BacktraceConfiguration configuration, BacktraceBreadcrumbs b
HandleNativeCrashes(clientAttributes, attachments);
INITIALIZED = true;
}
else
{
PendingCrashReportQuarantine.SetCaptureActive(false);
}
if (_configuration.HandleANR)
{
HandleAnr();
Expand All @@ -93,6 +97,7 @@ private void HandleNativeCrashes(IDictionary<string, string> attributes, IEnumer
if (string.IsNullOrEmpty(databasePath) || !Directory.Exists(databasePath))
{
Debug.LogWarning("Backtrace native integration status: database path doesn't exist");
PendingCrashReportQuarantine.SetCaptureActive(false);
return;
}

Expand All @@ -105,7 +110,11 @@ private void HandleNativeCrashes(IDictionary<string, string> attributes, IEnumer
var attributeKeys = attributes.Keys.ToArray();
var attributeValues = attributes.Values.ToArray();

// hand the previous session's quarantined report back to PLCrashReporter right before it starts, so the report is submitted and purged as usual
// see PendingCrashReportQuarantine for the Unity built-in crash reporter conflict
PendingCrashReportQuarantine.RestoreLiveReport(PendingCrashReportQuarantine.GetApplicationReportDirectory());
Start(plcrashreporterUrl.ToString(), attributeKeys, attributeValues, attributeValues.Length, _configuration.OomReports, attachments.ToArray(), attachments.Count(), _configuration.ClientSideUnwinding);
PendingCrashReportQuarantine.SetCaptureActive(true);
CaptureNativeCrashes = true;
}

Expand Down Expand Up @@ -270,6 +279,7 @@ public override void Disable()
if (CaptureNativeCrashes)
{
CaptureNativeCrashes = false;
PendingCrashReportQuarantine.SetCaptureActive(false);
DisableNativeIntegration();
}
base.Disable();
Expand Down
183 changes: 183 additions & 0 deletions Runtime/Native/OSX/PendingCrashReportQuarantine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
using System;
using System.IO;
using UnityEngine;

namespace Backtrace.Unity.Runtime.Native.OSX
{
/// <summary>
/// Guards Backtrace's pending macOS crash reports against Unity's built-in crash reporter.
/// </summary>
internal static class PendingCrashReportQuarantine
{
/// <summary>
/// PLCrashReporter's fixed pending report file name.
/// </summary>
internal const string LiveReportFileName = "live_report.plcrash";

/// <summary>
/// Name the pending report is parked under while hidden from Unity.
/// </summary>
internal const string QuarantinedReportFileName = LiveReportFileName + ".backtrace-quarantine";

/// <summary>
/// PlayerPrefs flag persisted while the Backtrace native crash handler is active, so the next launch knows a pending report in the shared directory belongs to this SDK.
/// </summary>
internal const string CaptureActivePlayerPrefsKey = "backtrace-osx-native-capture";

/// <summary>
/// PLCrashReporter's shared cache directory name.
/// </summary>
private const string PlCrashReporterCacheDirectory = "com.plausiblelabs.crashreporter.data";

/// <summary>
/// PLCrashReporter's per-application report directory under the given caches root.
/// </summary>
internal static string GetDefaultReportDirectory(string cachesRoot, string bundleIdentifier)
{
return Path.Combine(Path.Combine(cachesRoot, PlCrashReporterCacheDirectory), bundleIdentifier);
}

/// <summary>
/// Default PLCrashReporter report directory of the running application, or null when it cannot be determined. Never throws.
/// </summary>
internal static string GetApplicationReportDirectory()
{
try
{
var bundleIdentifier = Application.identifier;
if (string.IsNullOrEmpty(bundleIdentifier))
{
return null;
}
var home = Environment.GetFolderPath(Environment.SpecialFolder.Personal);
if (string.IsNullOrEmpty(home))
{
return null;
}
// PLCrashReporter's default basePath is NSCachesDirectory
var cachesRoot = Path.Combine(Path.Combine(home, "Library"), "Caches");
return GetDefaultReportDirectory(cachesRoot, bundleIdentifier);
}
catch (Exception)
{
return null;
}
}

/// <summary>
/// Moves a pending report aside so Unity's crash reporter cannot find it. Never throws.
/// </summary>
/// <returns>true when a report was quarantined</returns>
internal static bool QuarantineLiveReport(string reportDirectory)
{
try
{
if (string.IsNullOrEmpty(reportDirectory))
{
return false;
}
var liveReport = Path.Combine(reportDirectory, LiveReportFileName);
if (!File.Exists(liveReport))
{
return false;
}
var quarantinedReport = Path.Combine(reportDirectory, QuarantinedReportFileName);
if (File.Exists(quarantinedReport))
{
// a previously quarantined report was never handed back, keep the newer crash, matching PLCrashReporter's single-pending-report semantics
File.Delete(quarantinedReport);
}
File.Move(liveReport, quarantinedReport);
return true;
}
catch (Exception)
{
return false;
}
}

/// <summary>
/// Puts a quarantined report back so PLCrashReporter can submit and purge it. Call immediately before the native crash handler starts. Never throws.
/// </summary>
/// <returns>true when a report was restored</returns>
internal static bool RestoreLiveReport(string reportDirectory)
{
try
{
if (string.IsNullOrEmpty(reportDirectory))
{
return false;
}
var quarantinedReport = Path.Combine(reportDirectory, QuarantinedReportFileName);
if (!File.Exists(quarantinedReport))
{
return false;
}
var liveReport = Path.Combine(reportDirectory, LiveReportFileName);
if (File.Exists(liveReport))
{
// a fresh report already occupies the live slot, keep the quarantined one parked for the next launch instead of overwriting either of them
return false;
}
File.Move(quarantinedReport, liveReport);
return true;
}
catch (Exception)
{
return false;
}
}

/// <summary>
/// Persists whether Backtrace native crash capture is active, so the next launch only quarantines reports this SDK is responsible for.
/// Saved immediately because a crashed session never reaches Unity's regular PlayerPrefs flush. Never throws.
/// </summary>
internal static void SetCaptureActive(bool active)
{
try
{
var value = active ? 1 : 0;
if (PlayerPrefs.GetInt(CaptureActivePlayerPrefsKey, -1) == value)
{
return;
}
PlayerPrefs.SetInt(CaptureActivePlayerPrefsKey, value);
PlayerPrefs.Save();
}
catch (Exception)
{
}
}

/// <summary>
/// True when the previous session had Backtrace native crash capture active.
/// </summary>
internal static bool IsCaptureActive()
{
try
{
return PlayerPrefs.GetInt(CaptureActivePlayerPrefsKey, 0) == 1;
}
catch (Exception)
{
return false;
}
}

#if UNITY_STANDALONE_OSX && !UNITY_EDITOR
#if UNITY_2019_2_OR_NEWER
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
#else
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
#endif
private static void QuarantineOnStartup()
{
if (!IsCaptureActive())
{
return;
}
QuarantineLiveReport(GetApplicationReportDirectory());
}
#endif
}
}
11 changes: 11 additions & 0 deletions Runtime/Native/OSX/PendingCrashReportQuarantine.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

155 changes: 155 additions & 0 deletions Tests/Runtime/Native/PendingCrashReportQuarantineTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
using System;
using System.IO;
using Backtrace.Unity.Runtime.Native.OSX;
using NUnit.Framework;
using UnityEngine;

namespace Backtrace.Unity.Tests.Runtime
{
public class PendingCrashReportQuarantineTests
{
private string _reportDirectory;

[SetUp]
public void Setup()
{
_reportDirectory = Path.Combine(Path.GetTempPath(), "backtrace-quarantine-tests-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_reportDirectory);
}

[TearDown]
public void Cleanup()
{
if (Directory.Exists(_reportDirectory))
{
Directory.Delete(_reportDirectory, true);
}
}

private string LiveReportPath
{
get { return Path.Combine(_reportDirectory, PendingCrashReportQuarantine.LiveReportFileName); }
}

private string QuarantinedReportPath
{
get { return Path.Combine(_reportDirectory, PendingCrashReportQuarantine.QuarantinedReportFileName); }
}

[Test]
public void Quarantine_MovesPendingReportOutOfUnitysSight()
{
File.WriteAllText(LiveReportPath, "pending-report");

Assert.IsTrue(PendingCrashReportQuarantine.QuarantineLiveReport(_reportDirectory));

Assert.IsFalse(File.Exists(LiveReportPath));
Assert.AreEqual("pending-report", File.ReadAllText(QuarantinedReportPath));
}

[Test]
public void Quarantine_WithoutPendingReport_DoesNothing()
{
Assert.IsFalse(PendingCrashReportQuarantine.QuarantineLiveReport(_reportDirectory));

Assert.IsFalse(File.Exists(LiveReportPath));
Assert.IsFalse(File.Exists(QuarantinedReportPath));
}

[Test]
public void Quarantine_ReplacesStaleQuarantinedReportWithNewerCrash()
{
File.WriteAllText(QuarantinedReportPath, "stale-report");
File.WriteAllText(LiveReportPath, "newer-report");

Assert.IsTrue(PendingCrashReportQuarantine.QuarantineLiveReport(_reportDirectory));

Assert.IsFalse(File.Exists(LiveReportPath));
Assert.AreEqual("newer-report", File.ReadAllText(QuarantinedReportPath));
}

[Test]
public void Restore_HandsQuarantinedReportBackToPlCrashReporter()
{
File.WriteAllText(QuarantinedReportPath, "pending-report");

Assert.IsTrue(PendingCrashReportQuarantine.RestoreLiveReport(_reportDirectory));

Assert.IsFalse(File.Exists(QuarantinedReportPath));
Assert.AreEqual("pending-report", File.ReadAllText(LiveReportPath));
}

[Test]
public void Restore_WithoutQuarantinedReport_DoesNothing()
{
Assert.IsFalse(PendingCrashReportQuarantine.RestoreLiveReport(_reportDirectory));

Assert.IsFalse(File.Exists(LiveReportPath));
Assert.IsFalse(File.Exists(QuarantinedReportPath));
}

[Test]
public void Restore_KeepsBothReportsWhenAFreshLiveReportExists()
{
File.WriteAllText(QuarantinedReportPath, "quarantined-report");
File.WriteAllText(LiveReportPath, "fresh-report");

Assert.IsFalse(PendingCrashReportQuarantine.RestoreLiveReport(_reportDirectory));

Assert.AreEqual("fresh-report", File.ReadAllText(LiveReportPath));
Assert.AreEqual("quarantined-report", File.ReadAllText(QuarantinedReportPath));
}

[Test]
public void QuarantineAndRestore_NeverThrowForInvalidDirectories()
{
var missingDirectory = Path.Combine(_reportDirectory, "does-not-exist");

Assert.DoesNotThrow(() => PendingCrashReportQuarantine.QuarantineLiveReport(null));
Assert.DoesNotThrow(() => PendingCrashReportQuarantine.RestoreLiveReport(null));
Assert.IsFalse(PendingCrashReportQuarantine.QuarantineLiveReport(missingDirectory));
Assert.IsFalse(PendingCrashReportQuarantine.RestoreLiveReport(missingDirectory));
}

[Test]
public void DefaultReportDirectory_FollowsPlCrashReporterLayout()
{
var expected = Path.Combine(Path.Combine("caches", "com.plausiblelabs.crashreporter.data"), "com.example.app");

Assert.AreEqual(expected, PendingCrashReportQuarantine.GetDefaultReportDirectory("caches", "com.example.app"));
}

[Test]
public void ApplicationReportDirectory_NeverThrows()
{
Assert.DoesNotThrow(() => PendingCrashReportQuarantine.GetApplicationReportDirectory());
}

[Test]
public void CaptureActiveFlag_RoundTripsThroughPlayerPrefs()
{
var hadKey = PlayerPrefs.HasKey(PendingCrashReportQuarantine.CaptureActivePlayerPrefsKey);
var previousValue = hadKey ? PlayerPrefs.GetInt(PendingCrashReportQuarantine.CaptureActivePlayerPrefsKey) : 0;
try
{
PendingCrashReportQuarantine.SetCaptureActive(true);
Assert.IsTrue(PendingCrashReportQuarantine.IsCaptureActive());

PendingCrashReportQuarantine.SetCaptureActive(false);
Assert.IsFalse(PendingCrashReportQuarantine.IsCaptureActive());
}
finally
{
if (hadKey)
{
PlayerPrefs.SetInt(PendingCrashReportQuarantine.CaptureActivePlayerPrefsKey, previousValue);
}
else
{
PlayerPrefs.DeleteKey(PendingCrashReportQuarantine.CaptureActivePlayerPrefsKey);
}
PlayerPrefs.Save();
}
}
}
}
Loading
Loading