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