From efa65790be4f5100f2002f869557b4cd1f11a6fc Mon Sep 17 00:00:00 2001 From: Kevin Yockey Date: Sat, 30 Jan 2021 11:38:27 -0800 Subject: [PATCH 1/2] Attempt to gracefully handle some unhandled exceptions. --- Assets/DeltaDNA/Runtime/DDNAImpl.cs | 37 +++++-- .../DeltaDNA/Runtime/Helpers/EngageCache.cs | 104 ++++++++++++++---- Assets/DeltaDNA/Runtime/Helpers/EventStore.cs | 63 ++++++++++- Assets/DeltaDNA/Runtime/Helpers/Network.cs | 18 +++ 4 files changed, 187 insertions(+), 35 deletions(-) diff --git a/Assets/DeltaDNA/Runtime/DDNAImpl.cs b/Assets/DeltaDNA/Runtime/DDNAImpl.cs index 8d62cd86..9a302441 100644 --- a/Assets/DeltaDNA/Runtime/DDNAImpl.cs +++ b/Assets/DeltaDNA/Runtime/DDNAImpl.cs @@ -341,18 +341,31 @@ override internal void RecordPushNotification(Dictionary payload override internal void RequestSessionConfiguration() { Logger.LogDebug("Requesting session configuration"); - var firstSession = PlayerPrefs.HasKey(DDNA.PF_KEY_FIRST_SESSION) - ? DateTime.ParseExact( - PlayerPrefs.GetString(DDNA.PF_KEY_FIRST_SESSION), - Settings.EVENT_TIMESTAMP_FORMAT, - CultureInfo.InvariantCulture) - : (DateTime?) null; - var lastSession = PlayerPrefs.HasKey(DDNA.PF_KEY_LAST_SESSION) - ? DateTime.ParseExact( - PlayerPrefs.GetString(DDNA.PF_KEY_LAST_SESSION), - Settings.EVENT_TIMESTAMP_FORMAT, - CultureInfo.InvariantCulture) - : (DateTime?) null; + // The session key is becoming invalid somehow. An empty string in PlayerPrefs would do that... + // FormatException: String was not recognized as a valid DateTime. + // at System.DateTimeParse.ParseExact (System.String s, System.String format, System.Globalization.DateTimeFormatInfo dtfi, System.Globalization.DateTimeStyles style) + // at DeltaDNA.DDNAImpl.RequestSessionConfiguration () + // at DeltaDNA.DDNA.NewSession () + // at DeltaDNA.DDNAImpl.StartSDK (System.Boolean newPlayer) + // at DeltaDNA.DDNA.StartSDK (DeltaDNA.Configuration config, System.String userID) + DateTime? firstSession = null, lastSession = null; + + try { + firstSession = PlayerPrefs.HasKey(DDNA.PF_KEY_FIRST_SESSION) + ? DateTime.ParseExact( + PlayerPrefs.GetString(DDNA.PF_KEY_FIRST_SESSION), + Settings.EVENT_TIMESTAMP_FORMAT, + CultureInfo.InvariantCulture) + : (DateTime?)null; + lastSession = PlayerPrefs.HasKey(DDNA.PF_KEY_LAST_SESSION) + ? DateTime.ParseExact( + PlayerPrefs.GetString(DDNA.PF_KEY_LAST_SESSION), + Settings.EVENT_TIMESTAMP_FORMAT, + CultureInfo.InvariantCulture) + : (DateTime?)null; + } catch (Exception ex) { + Logger.LogError("Unable to parse session times: " + ex); + } Engagement engagement = new Engagement("config") { Flavour = "internal" }; engagement.AddParam("timeSinceFirstSession", firstSession != null diff --git a/Assets/DeltaDNA/Runtime/Helpers/EngageCache.cs b/Assets/DeltaDNA/Runtime/Helpers/EngageCache.cs index 185b576f..814800da 100644 --- a/Assets/DeltaDNA/Runtime/Helpers/EngageCache.cs +++ b/Assets/DeltaDNA/Runtime/Helpers/EngageCache.cs @@ -38,18 +38,51 @@ internal EngageCache(Settings settings){ this.settings = settings; lock (LOCK) { - CreateDirectory(); - - cache = Directory - .GetFiles(location) - .ToDictionary(e => Path.GetFileName(e), e => File.ReadAllText(e)); - if (File.Exists(location + TIMES)) { - times = File - .ReadAllLines(location + TIMES) - .ToDictionary( - e => e.Split(' ')[0], - e => new DateTime(Convert.ToInt64(e.Split(' ')[1]))); - } else { + // Handle the case where the disk is full or can't be written to + // IOException: Disk full. Path /var/mobile/Containers/Data/Application/[HASH]/Library/Caches/deltadna + // at System.IO.Directory.CreateDirectoriesInternal (System.String path) + // at System.IO.Directory.CreateDirectoriesInternal (System.String path) + // at DeltaDNA.EngageCache..ctor (DeltaDNA.Settings settings) + // at DeltaDNA.DDNAImpl..ctor (DeltaDNA.DDNA ddna) + // at DeltaDNA.DDNA.Awake () + // at UnityEngine.GameObject.AddComponent[T] () + // at DeltaDNA.Singleton`1[T].get_Instance () + try { + CreateDirectory(); + } catch (Exception ex) { + Logger.LogWarning("Unable to create directory " + location + ": " + ex); + cache = new Dictionary(); + times = new Dictionary(); + return; + } + + // Handle the case where cached data is not in the expected format for some reason + // IndexOutOfRangeException: Index was outside the bounds of the array. + // at DeltaDNA.EngageCache+<>c.<.ctor>b__6_3 (System.String e) + // at System.Func`2[T,TResult].Invoke (T arg) + // at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement] (System.Collections.Generic.IEnumerable`1[T] source, System.Func`2[T,TResult] keySelector, System.Func`2[T,TResult] elementSelector, System.Collections.Generic.IEqualityComparer`1[T] comparer) + // at System.Linq.Enumerable.ToDictionary[TSource,TKey,TElement] (System.Collections.Generic.IEnumerable`1[T] source, System.Func`2[T,TResult] keySelector, System.Func`2[T,TResult] elementSelector) + // at DeltaDNA.EngageCache..ctor (DeltaDNA.Settings settings) + // at DeltaDNA.DDNAImpl..ctor (DeltaDNA.DDNA ddna) + // at DeltaDNA.DDNA.Awake () + // at UnityEngine.GameObject.AddComponent[T] () + // at DeltaDNA.Singleton`1[T].get_Instance () + try { + cache = Directory + .GetFiles(location) + .ToDictionary(e => Path.GetFileName(e), e => File.ReadAllText(e)); + if (File.Exists(location + TIMES)) { + times = File + .ReadAllLines(location + TIMES) + .ToDictionary( + e => e.Split(' ')[0], + e => new DateTime(Convert.ToInt64(e.Split(' ')[1]))); + } else { + times = new Dictionary(); + } + } catch (Exception ex) { + Logger.LogError("Unable to deserialize cache: " + ex); + cache = new Dictionary(); times = new Dictionary(); } } @@ -103,15 +136,48 @@ internal string Get(string decisionPoint, string flavour) { internal void Save() { lock (LOCK) { - CreateDirectory(); - - foreach (var item in cache) { - File.WriteAllText(location + item.Key, item.Value); + // Handle the case where the disk is full or can't be written to + // IOException: Disk full. Path /storage/emulated/0/Android/data/[BUNDLE_ID]/cache/deltadna + // at System.IO.Directory.CreateDirectoriesInternal (System.String path) + // at System.IO.Directory.CreateDirectoriesInternal (System.String path) + // at DeltaDNA.EngageCache.Save () + // at DeltaDNA.DDNAImpl.OnApplicationPause (System.Boolean pauseStatus) + try { + CreateDirectory(); + } catch (Exception ex) { + Logger.LogError("Unable to create directory " + location + ": " + ex); + return; } - File.WriteAllLines( - location + TIMES, - times.Select(e => e.Key + ' ' + e.Value.Ticks).ToArray()); + // Handle the case where the disk is full or can't be written to + // IOException: Disk full. Path /var/mobile/Containers/Data/Application/[HASH]/Library/Caches/deltadna/engagements/times + // at System.IO.FileStream.FlushBuffer () + // at System.IO.StreamWriter.Dispose (System.Boolean disposing) + // at System.IO.TextWriter.Dispose () + // at System.IO.File.WriteAllLines (System.String path, System.String[] contents) + // at DeltaDNA.EngageCache.Save () + // at DeltaDNA.DDNAImpl.OnApplicationPause (System.Boolean pauseStatus) + // + // IOException: Disk full. Path /storage/emulated/0/Android/data/[BUNDLE_ID]/cache/deltadna/engagements/times + // at System.IO.FileStream.FlushBuffer () + // at System.IO.FileStream.Dispose (System.Boolean disposing) + // at System.IO.Stream.Close () + // at System.IO.StreamWriter.Dispose (System.Boolean disposing) + // at System.IO.TextWriter.Dispose () + // at System.IO.File.WriteAllText (System.String path, System.String contents, System.Text.Encoding encoding) + // at DeltaDNA.EngageCache.Save () + // at DeltaDNA.DDNAImpl.OnApplicationPause (System.Boolean pauseStatus) + try { + foreach (var item in cache) { + File.WriteAllText(location + item.Key, item.Value); + } + + File.WriteAllLines( + location + TIMES, + times.Select(e => e.Key + ' ' + e.Value.Ticks).ToArray()); + } catch (Exception ex) { + Logger.LogError("Unable to write cache: " + ex); + } } } diff --git a/Assets/DeltaDNA/Runtime/Helpers/EventStore.cs b/Assets/DeltaDNA/Runtime/Helpers/EventStore.cs index b37a8c70..8f1961e7 100644 --- a/Assets/DeltaDNA/Runtime/Helpers/EventStore.cs +++ b/Assets/DeltaDNA/Runtime/Helpers/EventStore.cs @@ -172,8 +172,39 @@ public void FlushBuffers() { if (_initialised) { - _infs.Flush(); - _outfs.Flush(); + // Convert to MemoryStream if there's issues writing the data to disk + // IOException: Disk full. Path /storage/emulated/0/Android/data/[BUNDLE_ID]/files/ddsdk/events/B + // at System.IO.FileStream.FlushBuffer () + // at DeltaDNA.EventStore.FlushBuffers () + // at DeltaDNA.DDNAImpl.OnApplicationPause (System.Boolean pauseStatus) + try { + _infs.Flush(); + } catch (Exception ex) { + Logger.LogError("Unable to flush \"in\" buffer, converting to MemoryStream: " + ex); + try { + _infs.Dispose(); + } catch { + } finally { + _infs = new MemoryStream(); + } + } + + // Convert to MemoryStream if there's issues writing the data to disk + // IOException: Disk full. Path /storage/emulated/0/Android/data/[BUNDLE_ID]/files/ddsdk/events/B + // at System.IO.FileStream.FlushBuffer () + // at DeltaDNA.EventStore.FlushBuffers () + // at DeltaDNA.DDNAImpl.OnApplicationPause (System.Boolean pauseStatus) + try { + _outfs.Flush(); + } catch (Exception ex) { + Logger.LogError("Unable to flush \"out\" buffer, converting to MemoryStream: " + ex); + try { + _outfs.Dispose(); + } catch { + } finally { + _outfs = new MemoryStream(); + } + } } } } @@ -304,8 +335,32 @@ public static void ReadEvents(Stream stream, IList events) public static void SwapStreams(ref Stream sin, ref Stream sout) { - // Close off our write stream - sin.Flush(); + // Convert to MemoryStream if there's issues writing the data to disk + // UnauthorizedAccessException: Access to the path "/storage/emulated/0/Android/data/[BUNDLE_ID]/files/ddsdk/events/B" is denied. + // at System.IO.FileStream.FlushBuffer () + // at DeltaDNA.EventStore.SwapStreams (System.IO.Stream& sin, System.IO.Stream& sout) + // at DeltaDNA.EventStore.Swap () + // at DeltaDNA.DDNAImpl+d__47.MoveNext () + // at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) + // + // IOException: Disk full. Path /storage/emulated/0/Android/data/[BUNDLE_ID]/files/ddsdk/events/B + // at System.IO.FileStream.FlushBuffer () + // at DeltaDNA.EventStore.SwapStreams (System.IO.Stream& sin, System.IO.Stream& sout) + // at DeltaDNA.EventStore.Swap () + // at DeltaDNA.DDNAImpl+d__47.MoveNext () + // at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) + try { + // Close off our write stream + sin.Flush(); + } catch (Exception ex) { + Logger.LogError("Unable to flush to disk: " + ex); + try { + sin.Dispose(); + } catch { + } finally { + sin = new MemoryStream(); + } + } // Swap the file handles Stream tmp = sin; sin = sout; diff --git a/Assets/DeltaDNA/Runtime/Helpers/Network.cs b/Assets/DeltaDNA/Runtime/Helpers/Network.cs index 9d1a782e..1b96adcb 100644 --- a/Assets/DeltaDNA/Runtime/Helpers/Network.cs +++ b/Assets/DeltaDNA/Runtime/Helpers/Network.cs @@ -73,6 +73,24 @@ internal static class Network { internal static IEnumerator SendRequest(HttpRequest request, Action completionHandler) { + // If the request body is null then the request is going to fail on Encoding.UTF8.GetBytes(). This will be + // null if EngageRequest.ToJSON fails. 1001 is an error code used further down. + // ArgumentNullException: String reference not set to an instance of a String.Parameter name: s + // at System.Text.Encoding.GetBytes (System.String s) + // at DeltaDNA.Network+d__3.MoveNext () + // at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) + // at DeltaDNA.Engage+d__0.MoveNext () + // at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) + // at DeltaDNA.DDNAImpl.RequestEngagement (DeltaDNA.Engagement engagement, System.Action`1[T] callback) + // at DeltaDNA.DDNAImpl.RequestSessionConfiguration () + // at DeltaDNA.DDNA.NewSession () + // at DeltaDNA.DDNAImpl.StartSDK (System.Boolean newPlayer) + // at DeltaDNA.DDNA.StartSDK (DeltaDNA.Configuration config, System.String userID) + if (request.HTTPBody == null) { + completionHandler?.Invoke(1001, null, "Invalid HTTPBody"); + yield break; + } + // timeout feature added in 5.6.2f1 #if UNITY_5_6_OR_NEWER && !UNITY_5_6_0 && !UNITY_5_6_1 From 06ab3f7fc759989dea33580de128d09e89151457 Mon Sep 17 00:00:00 2001 From: Kevin Yockey Date: Tue, 2 Feb 2021 09:18:26 -0800 Subject: [PATCH 2/2] HTTPBody null check should only be done on POST requests. --- Assets/DeltaDNA/Runtime/Helpers/Network.cs | 36 +++++++++++----------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Assets/DeltaDNA/Runtime/Helpers/Network.cs b/Assets/DeltaDNA/Runtime/Helpers/Network.cs index 1b96adcb..f4621697 100644 --- a/Assets/DeltaDNA/Runtime/Helpers/Network.cs +++ b/Assets/DeltaDNA/Runtime/Helpers/Network.cs @@ -73,24 +73,6 @@ internal static class Network { internal static IEnumerator SendRequest(HttpRequest request, Action completionHandler) { - // If the request body is null then the request is going to fail on Encoding.UTF8.GetBytes(). This will be - // null if EngageRequest.ToJSON fails. 1001 is an error code used further down. - // ArgumentNullException: String reference not set to an instance of a String.Parameter name: s - // at System.Text.Encoding.GetBytes (System.String s) - // at DeltaDNA.Network+d__3.MoveNext () - // at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) - // at DeltaDNA.Engage+d__0.MoveNext () - // at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) - // at DeltaDNA.DDNAImpl.RequestEngagement (DeltaDNA.Engagement engagement, System.Action`1[T] callback) - // at DeltaDNA.DDNAImpl.RequestSessionConfiguration () - // at DeltaDNA.DDNA.NewSession () - // at DeltaDNA.DDNAImpl.StartSDK (System.Boolean newPlayer) - // at DeltaDNA.DDNA.StartSDK (DeltaDNA.Configuration config, System.String userID) - if (request.HTTPBody == null) { - completionHandler?.Invoke(1001, null, "Invalid HTTPBody"); - yield break; - } - // timeout feature added in 5.6.2f1 #if UNITY_5_6_OR_NEWER && !UNITY_5_6_0 && !UNITY_5_6_1 @@ -100,6 +82,24 @@ internal static IEnumerator SendRequest(HttpRequest request, Actiond__3.MoveNext () + // at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) + // at DeltaDNA.Engage+d__0.MoveNext () + // at UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) + // at DeltaDNA.DDNAImpl.RequestEngagement (DeltaDNA.Engagement engagement, System.Action`1[T] callback) + // at DeltaDNA.DDNAImpl.RequestSessionConfiguration () + // at DeltaDNA.DDNA.NewSession () + // at DeltaDNA.DDNAImpl.StartSDK (System.Boolean newPlayer) + // at DeltaDNA.DDNA.StartSDK (DeltaDNA.Configuration config, System.String userID) + if (request.HTTPBody == null) { + completionHandler?.Invoke(1001, null, "Invalid HTTPBody"); + yield break; + } + www.method = UnityWebRequest.kHttpVerbPOST; foreach (var entry in request.getHeaders()) {