diff --git a/CHANGELOG.md b/CHANGELOG.md index b9fca17..e31037f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ * Needs "Countly.UI.WebView2" for displaying content on WPF. * Fixed a bug where the request queue was not processed when automatic user property saving ("autoSendUserDetails") was disabled, so queued requests (for example from a manual session update) were never uploaded and could cause the SDK to loop indefinitely. * Fixed a bug where a user-details change that serialized to nothing (empty user details) left the internal "changed" flag set forever, so the SDK kept re-processing user details on every session call and upload-completion checks could wait indefinitely. +* Fixed a crash ("NullReferenceException") when a session-timer tick landed while/after "Halt" cleared the SDK state (an unhandled exception on a timer thread can terminate the host application), and when "SessionUpdate" was called before the first "Init". ## 25.4.3 * ! Minor breaking change ! User properties will now be automatically saved under the following conditions: diff --git a/countlyCommon/TestingRelated/SessionTests.cs b/countlyCommon/TestingRelated/SessionTests.cs index d4ca590..6686cc1 100644 --- a/countlyCommon/TestingRelated/SessionTests.cs +++ b/countlyCommon/TestingRelated/SessionTests.cs @@ -31,6 +31,31 @@ public void Dispose() } + [Fact] + /// + /// A session-timer tick that lands after Halt must not throw: Halt nulls the modules while + /// the old Configuration object (here with backend mode on) stays visible to the in-flight tick. + /// + public void UpdateSession_TimerTickAfterHalt_DoesNotThrow() + { + CountlyConfig cc = TestHelper.GetConfig(); + cc.EnableBackendMode(); + Countly.Instance.Init(cc).Wait(); + Countly.Instance.HaltInternal().Wait(); + + // the body the session timer runs; used to NRE on the nulled moduleBackendMode + Countly.Instance.UpdateSessionInternal().Wait(); + } + + [Fact] + /// Calling SessionUpdate before the first Init (Configuration is null) must not throw. + public void SessionUpdate_BeforeInit_DoesNotThrow() + { + Countly.Instance.Configuration = null; // what a fresh, never-initialized process looks like + + Countly.Instance.SessionUpdate(10).Wait(); + } + private void ValidateSessionRequestParams(NameValueCollection collection, string appKey, string deviceId, string deviceIdType) { Assert.False(string.IsNullOrEmpty(collection.Get("sdk_version"))); diff --git a/countlyCommon/countlyCommon/CountlyBase.cs b/countlyCommon/countlyCommon/CountlyBase.cs index c2dd897..e08a8a9 100644 --- a/countlyCommon/countlyCommon/CountlyBase.cs +++ b/countlyCommon/countlyCommon/CountlyBase.cs @@ -215,12 +215,24 @@ internal async Task SaveStoredRequests() } } - protected async Task UpdateSessionInternal(int? elapsedTime = null) + internal async Task UpdateSessionInternal(int? elapsedTime = null) { UtilityHelper.CountlyLogging("[CountlyBase] Session Update happening'"); - if (Configuration.backendMode) { - moduleBackendMode.OnTimer(); - Upload(); + + // A session-timer tick can land while/after Halt nulls the SDK state. Work on local + // captures and bail out on a halted or never-initialized SDK instead of crashing. + CountlyConfig config = Configuration; + if (config == null || !IsInitialized()) { + UtilityHelper.CountlyLogging("[CountlyBase] UpdateSessionInternal, SDK is not initialized (or halted), ignoring session update"); + return; + } + + if (config.backendMode) { + ModuleBackendMode backendMode = moduleBackendMode; + if (backendMode != null) { + backendMode.OnTimer(); + Upload(); + } return; } @@ -1760,14 +1772,16 @@ public async Task SessionBegin() /// public async Task SessionUpdate(int elapsedTimeSeconds) { - if (Configuration.backendMode) { - UtilityHelper.CountlyLogging("[CountlyBase] SessionUpdate, Backend Mode enabled, returning"); + UtilityHelper.CountlyLogging("[CountlyBase] Calling 'SessionUpdate'"); + // The initialization check must come first: before the first Init (or after Halt) + // there is no Configuration object to read the backend-mode flag from. + if (!IsInitialized() || Configuration == null) { + UtilityHelper.CountlyLogging("[CountlyBase] SessionUpdate: SDK must initialized before calling 'SessionUpdate'"); return; } - UtilityHelper.CountlyLogging("[CountlyBase] Calling 'SessionUpdate'"); - if (!IsInitialized()) { - UtilityHelper.CountlyLogging("[CountlyBase] SessionUpdate: SDK must initialized before calling 'SessionUpdate'"); + if (Configuration.backendMode) { + UtilityHelper.CountlyLogging("[CountlyBase] SessionUpdate, Backend Mode enabled, returning"); return; } if (elapsedTimeSeconds < 0) {