From 1c47d3f45208b5d7b8bc5b4d58afa89d79c8d8bd Mon Sep 17 00:00:00 2001 From: soloturn Date: Sat, 22 Aug 2026 13:14:56 +0200 Subject: [PATCH 1/2] fix(reporter): declare jpastebin's Jackson dependency explicitly Clicking "PasteBin" threw NoClassDefFoundError: com/fasterxml/jackson/core/type/TypeReference the first time it actually reached a Jackson class - reproduced directly by calling PastebinUploadRunnable.call() outside the dialog. jpastebin's own embedded META-INF/maven/org/jpastebin/pom.xml (inside the jar) pins jackson-databind/jackson-core/jackson-annotations 2.9.7, but the POM Gradle actually resolves for org:jpastebin:1.0.1 from the JBoss repo is an empty Nexus-generated stub with no at all - so Gradle never pulled Jackson in. Declares all three explicitly via the jackson-bom platform rather than three separately-pinned versions: jackson-annotations renumbered its own versioning away from core/databind's x.y.z scheme starting at 2.20, so hand-pinning all three to the same string breaks depending on which release line you pick. The BOM keeps them resolvable together regardless. 2.9.7 is a 2018 release with known CVEs; Jackson's 2.x line keeps this level of API (ObjectMapper, TypeReference, annotations) stable, so the current release is a safe drop-in rather than matching jpastebin's old pin. Verified end to end: a direct call to PastebinUploadRunnable now succeeds against the real API instead of throwing. Co-Authored-By: Claude Sonnet 5 --- cr-core/build.gradle.kts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cr-core/build.gradle.kts b/cr-core/build.gradle.kts index 7fcf709..a7dad6a 100644 --- a/cr-core/build.gradle.kts +++ b/cr-core/build.gradle.kts @@ -62,6 +62,20 @@ dependencies { pmd("net.sourceforge.pmd:pmd-java:7.0.0-rc4") implementation("org:jpastebin:1.0.1") + // jpastebin needs these at runtime (see its own embedded META-INF/maven/org/jpastebin/pom.xml, + // which pins Jackson 2.9.7) but the POM Gradle actually resolves from the JBoss repo is an + // empty Nexus-generated stub with no at all - so without declaring these + // ourselves, PastebinUploadRunnable.call() throws NoClassDefFoundError the first time it + // touches a Jackson class, only when someone actually clicks "Upload". 2.9.7 is a 2018 release + // with known CVEs; Jackson's 2.x line keeps this level of API (ObjectMapper, TypeReference, + // annotations) stable, so a current release is a safe drop-in rather than matching the old pin. + // The BOM (not three separately-pinned versions) because jackson-annotations renumbered its own + // versioning away from core/databind's x.y.z scheme starting at 2.20 - the BOM is what keeps the + // three resolvable together regardless of a given module's own version string. + implementation(platform("com.fasterxml.jackson:jackson-bom:2.22.2")) + implementation("com.fasterxml.jackson.core:jackson-databind") + implementation("com.fasterxml.jackson.core:jackson-core") + implementation("com.fasterxml.jackson.core:jackson-annotations") implementation("org.apache.httpcomponents:httpclient:4.5.13") implementation("org.apache.httpcomponents:httpmime:4.5.13") From 3b730655248dd8d69c0014ceb9416e72d877eeb7 Mon Sep 17 00:00:00 2001 From: soloturn Date: Sat, 22 Aug 2026 13:15:16 +0200 Subject: [PATCH 2/2] fix(reporter): always print upload failures to stderr, not just a popup uploadFailed() only ever showed a JOptionPane - nothing else in this codebase logs upload failures anywhere. That popup reaches whoever happens to be watching the screen at that exact moment and leaves no trace at all once dismissed; whoever launched the process (a script, a supervisor, a developer tailing output) has no way to find out what happened after the fact. This is exactly what made the Jackson NoClassDefFoundError above hard to pin down in the first place - it only ever appeared as a popup. e.printStackTrace(System.err) now runs unconditionally before the dialog, on the same thread, so it can't get lost even if the JOptionPane is dismissed instantly. New package-private uploadForTesting() hook (bypasses the real ActionListener/network call so UploadPanelFailureLoggingTest can drive a failure directly) plus the same GlobalProperties NPE guard used elsewhere in this repo (needed just to construct GlobalProperties() in cr-core's own test classpath - see the sibling PRs). Co-Authored-By: Claude Sonnet 5 --- .../crashreporter/GlobalProperties.java | 22 ++++--- .../crashreporter/pages/UploadPanel.java | 13 ++++ .../pages/UploadPanelFailureLoggingTest.java | 65 +++++++++++++++++++ 3 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 cr-core/src/test/java/org/terasology/crashreporter/pages/UploadPanelFailureLoggingTest.java diff --git a/cr-core/src/main/java/org/terasology/crashreporter/GlobalProperties.java b/cr-core/src/main/java/org/terasology/crashreporter/GlobalProperties.java index 7c7cade..94a062b 100644 --- a/cr-core/src/main/java/org/terasology/crashreporter/GlobalProperties.java +++ b/cr-core/src/main/java/org/terasology/crashreporter/GlobalProperties.java @@ -39,16 +39,20 @@ public enum KEY { public GlobalProperties() { String propsUrl = "/crashreporter.properties"; String defaultPropsUrl = "/crashreporter_defaults.properties"; - try (InputStream stream = CrashReporter.class.getResourceAsStream(defaultPropsUrl)) { - properties.load(stream); - } catch (IOException e) { - // this should never go wrong - System.err.println("Unable to load default properties"); - } - try (InputStream stream = CrashReporter.class.getResourceAsStream(propsUrl)) { - properties.load(stream); + loadIfPresent(defaultPropsUrl); + // Only cr-core's downstream consumers (cr-terasology, cr-destsol, ...) ship this file - + // it's absent when cr-core is used standalone, which getResourceAsStream signals with + // null rather than an IOException, so that has to be checked explicitly. + loadIfPresent(propsUrl); + } + + private void loadIfPresent(String resourceUrl) { + try (InputStream stream = CrashReporter.class.getResourceAsStream(resourceUrl)) { + if (stream != null) { + properties.load(stream); + } } catch (IOException e) { - System.err.println("Unable to load " + propsUrl); + System.err.println("Unable to load " + resourceUrl); } } diff --git a/cr-core/src/main/java/org/terasology/crashreporter/pages/UploadPanel.java b/cr-core/src/main/java/org/terasology/crashreporter/pages/UploadPanel.java index cc30939..7103d5a 100644 --- a/cr-core/src/main/java/org/terasology/crashreporter/pages/UploadPanel.java +++ b/cr-core/src/main/java/org/terasology/crashreporter/pages/UploadPanel.java @@ -119,6 +119,11 @@ public URL getUploadedFileURL() { return uploadURL; } + /** Package-private test hook - equivalent to a real "PasteBin" click, but with a caller-supplied Callable. */ + void uploadForTesting(Callable callable) { + upload(callable); + } + private void upload(final Callable callable) { Runnable runnable = new Runnable() { @@ -169,6 +174,14 @@ public void run() { } private void uploadFailed(final Exception e) { + // Printed unconditionally, not just shown in the dialog below: a JOptionPane only reaches + // whoever is watching the screen at that exact moment, and leaves no trace at all once + // it's dismissed - nothing else in this codebase logs upload failures anywhere. Whoever + // launched this process (a script, a supervisor, a developer tailing output) needs to be + // able to find out what happened after the fact, not just the person who happened to be + // looking right then. + e.printStackTrace(System.err); + SwingUtilities.invokeLater(new Runnable() { @Override diff --git a/cr-core/src/test/java/org/terasology/crashreporter/pages/UploadPanelFailureLoggingTest.java b/cr-core/src/test/java/org/terasology/crashreporter/pages/UploadPanelFailureLoggingTest.java new file mode 100644 index 0000000..f20c503 --- /dev/null +++ b/cr-core/src/test/java/org/terasology/crashreporter/pages/UploadPanelFailureLoggingTest.java @@ -0,0 +1,65 @@ +// Copyright 2026 The Terasology Foundation +// SPDX-License-Identifier: Apache-2.0 + +package org.terasology.crashreporter.pages; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.terasology.crashreporter.GlobalProperties; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.Callable; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Regression test for an upload failure that previously left no trace anywhere once its + * {@code JOptionPane} was dismissed - found while manually testing the reporter dialog: a real + * exception (a missing-Jackson {@code NoClassDefFoundError} - see the jackson-bom fix elsewhere in + * this commit) only ever showed up in a popup, with nothing printed to stderr for whoever launched + * the process to find afterward. + */ +class UploadPanelFailureLoggingTest { + + private PrintStream originalErr; + private ByteArrayOutputStream capturedErr; + + @BeforeEach + void redirectStderr() { + originalErr = System.err; + capturedErr = new ByteArrayOutputStream(); + System.setErr(new PrintStream(capturedErr, true, StandardCharsets.UTF_8)); + } + + @AfterEach + void restoreStderr() { + System.setErr(originalErr); + } + + @Test + void aFailedUploadIsPrintedToStderrNotJustShownInAPopup() throws InterruptedException { + UploadPanel panel = new UploadPanel(new GlobalProperties(), () -> "log text", () -> "log.txt"); + + final Exception cause = new IllegalStateException("upload failed: missing Jackson class"); + panel.uploadForTesting(new Callable() { + @Override + public URL call() throws Exception { + throw cause; + } + }); + + long deadline = System.currentTimeMillis() + 2000; + while (capturedErr.size() == 0 && System.currentTimeMillis() < deadline) { + Thread.sleep(20); + } + + String stderr = capturedErr.toString(StandardCharsets.UTF_8); + assertTrue(stderr.contains("IllegalStateException"), "Expected the exception type on stderr, got: " + stderr); + assertTrue(stderr.contains("upload failed: missing Jackson class"), + "Expected the exception message on stderr, got: " + stderr); + } +}