Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## XX.XX.XX
* Fixed a bug where a NullPointerException in SDKCore.recover() would permanently block SDK initialization when a crash file from a previous session existed on disk.

## 24.1.4
* ! Minor breaking change ! User properties will now be automatically saved under the following conditions:
* When an event is recorded
Expand Down
118 changes: 118 additions & 0 deletions app-java/src/main/java/ly/count/java/demo/ReproduceIssue263.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package ly.count.java.demo;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import ly.count.sdk.java.Config;
import ly.count.sdk.java.Countly;
import org.json.JSONObject;

/**
* Reproducer for GitHub issue #263:
* "NullPointerException in SDKCore.recover() permanently blocks SDK initialization
* when a crash file exists"
*
* This simulates the scenario where:
* 1. A previous app run crashed and left a [CLY]_crash_* file on disk
* 2. The app restarts and calls Countly.init()
* 3. SDKCore.recover() finds the crash file and tries to process it
*
* WITHOUT the fix: NPE at networking.check(config) because networking is null
* WITH the fix: SDK initializes normally, crash is queued as a request
*/
public class ReproduceIssue263 {

public static void main(String[] args) {
String[] sdkStorageRootPath = { System.getProperty("user.home"), "__COUNTLY", "java_issue_263" };
File sdkStorageRootDirectory = new File(String.join(File.separator, sdkStorageRootPath));

if (!(sdkStorageRootDirectory.exists() && sdkStorageRootDirectory.isDirectory())) {
if (!sdkStorageRootDirectory.mkdirs()) {
System.out.println("[FAIL] Directory creation failed");
return;
}
}

// Step 1: Plant a fake crash file as if a previous run crashed
long crashTimestamp = System.currentTimeMillis() - 2000;
File crashFile = new File(sdkStorageRootDirectory, "[CLY]_crash_" + crashTimestamp);

JSONObject crashData = new JSONObject();
crashData.put("_error", "java.lang.RuntimeException: simulated crash from previous session\n"
+ "\tat com.example.App.doSomething(App.java:42)\n"
+ "\tat com.example.App.main(App.java:10)");
crashData.put("_nonfatal", false);
crashData.put("_os", "Java");
crashData.put("_os_version", System.getProperty("java.version"));
crashData.put("_device", "ReproducerDevice");
crashData.put("_resolution", "1920x1080");

try (BufferedWriter writer = Files.newBufferedWriter(crashFile.toPath())) {
writer.write(crashData.toString());
} catch (IOException e) {
System.out.println("[FAIL] Could not write crash file: " + e.getMessage());
return;
}

System.out.println("[INFO] Planted crash file: " + crashFile.getAbsolutePath());
System.out.println("[INFO] Crash file exists: " + crashFile.exists());
System.out.println();

// Step 2: Initialize SDK — this is where the NPE would occur
System.out.println("[TEST] Initializing SDK with crash file present...");
System.out.println("[TEST] If issue #263 is NOT fixed, you will see a NullPointerException below.");
System.out.println();

try {
Config config = new Config("https://test.server.ly", "TEST_APP_KEY", sdkStorageRootDirectory)
.setLoggingLevel(Config.LoggingLevel.DEBUG)
.enableFeatures(Config.Feature.CrashReporting, Config.Feature.Events, Config.Feature.Sessions);

Countly.instance().init(config);

System.out.println();
System.out.println("[PASS] SDK initialized successfully!");
System.out.println("[INFO] Crash file still exists: " + crashFile.exists());

if (!crashFile.exists()) {
System.out.println("[PASS] Crash file was processed and removed during recovery.");
} else {
System.out.println("[WARN] Crash file was NOT removed — recovery may have partially failed.");
}

// Check for request files (crash should be converted to a request)
File[] requestFiles = sdkStorageRootDirectory.listFiles(
(dir, name) -> name.startsWith("[CLY]_request_"));
if (requestFiles != null && requestFiles.length > 0) {
System.out.println("[PASS] Found " + requestFiles.length + " request file(s) — crash was queued for sending.");
}

// Clean shutdown
Countly.instance().halt();
System.out.println("[INFO] SDK stopped cleanly.");

} catch (NullPointerException e) {
System.out.println();
System.out.println("[FAIL] *** NullPointerException — Issue #263 is NOT fixed! ***");
System.out.println("[FAIL] " + e.getMessage());
e.printStackTrace();
} catch (Exception e) {
System.out.println();
System.out.println("[FAIL] Unexpected exception: " + e.getClass().getSimpleName() + ": " + e.getMessage());
e.printStackTrace();
} finally {
// Cleanup: remove test files
System.out.println();
System.out.println("[INFO] Cleaning up test directory...");
File[] files = sdkStorageRootDirectory.listFiles();
if (files != null) {
for (File f : files) {
f.delete();
}
}
sdkStorageRootDirectory.delete();
System.out.println("[INFO] Done.");
}
}
}
20 changes: 12 additions & 8 deletions sdk-java/src/main/java/ly/count/sdk/java/internal/SDKCore.java
Original file line number Diff line number Diff line change
Expand Up @@ -459,8 +459,6 @@ public void init(final InternalConfig givenConfig) {
modules.remove(feature);
}

recover(config);

if (config.isDefaultNetworking()) {
networking = new DefaultNetworking();

Expand Down Expand Up @@ -513,17 +511,23 @@ public Integer remaningRequests() {
}
}

recover(config);

user = new UserImpl(config);
initFinished(config);
}

private void initFinished(final InternalConfig config) {
modules.forEach((feature, module) -> module.initFinished(config));
if (config.isDefaultNetworking()) {
private void checkNetworking(InternalConfig config) {
if (networking != null) {
networking.check(config);
}
}

private void initFinished(final InternalConfig config) {
modules.forEach((feature, module) -> module.initFinished(config));
checkNetworking(config);
}

public UserImpl user() {
return user;
}
Expand Down Expand Up @@ -650,13 +654,13 @@ protected void recover(InternalConfig config) {

public void onSignal(InternalConfig config, int id) {
if (id == Signal.DID.getIndex()) {
networking.check(config);
checkNetworking(config);
}
}

public void onSignal(InternalConfig config, int id, String param) {
if (id == Signal.Ping.getIndex()) {
networking.check(config);
checkNetworking(config);
} else if (id == Signal.Crash.getIndex()) {
processCrash(config, Long.parseLong(param));
}
Expand All @@ -679,7 +683,7 @@ private boolean processCrash(InternalConfig config, Long id) {

if (Storage.push(config, request)) {
L.i("[SDKCore] Added request " + request.storageId() + " instead of crash " + crash.storageId());
networking.check(config);
checkNetworking(config);
Boolean success = Storage.remove(config, crash);
return (success != null) && success;
} else {
Expand Down
Loading
Loading