From ef4ad75f97f343d36a4e3a347235c1a5f99500cd Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 10 May 2026 09:04:50 +0300 Subject: [PATCH 1/2] maven: surface real Ant/server error in AntExecutor failures The catch-all in AntExecutor threw a hardcoded "Unable to restart the IEHS App" RuntimeException for every BuildException, masking the actual cause. When a server build fails (e.g. an HTTP 500 from the build server) the build client prints the response body and JSON error to stdout but does not propagate it through the exception chain, so the maven user only saw the misleading message. Tee stdout/stderr while the Ant project runs, scan the captured output for the server-error markers ("Response message from server is:", "Server Detailed Error Message:", "Server returned HTTP response code:") and include them in the thrown RuntimeException's message alongside the underlying BuildException's message. Console output is still forwarded to the original streams so the user-visible build log is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../java/com/codename1/ant/AntExecutor.java | 163 ++++++++++++++---- .../com/codename1/ant/AntExecutorTest.java | 43 +++++ 2 files changed, 174 insertions(+), 32 deletions(-) create mode 100644 maven/codenameone-maven-plugin/src/test/java/com/codename1/ant/AntExecutorTest.java diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/ant/AntExecutor.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/ant/AntExecutor.java index cd610f6b4e..beef50954d 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/ant/AntExecutor.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/ant/AntExecutor.java @@ -1,6 +1,10 @@ package com.codename1.ant; +import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; import java.util.Properties; import org.apache.tools.ant.BuildException; @@ -31,47 +35,102 @@ public static boolean executeAntTask(String buildXmlFileFullPath) { public static boolean executeAntTask(String buildXmlFileFullPath, String target, Properties properties) { boolean success = false; - DefaultLogger consoleLogger = getConsoleLogger(); - // Prepare Ant project - Project project = new Project(); - File buildFile = new File(buildXmlFileFullPath); + // Tee stdout/stderr so that, on failure, we can recover server-reported + // error details (such as the JSON body returned by the build server) that + // the build client prints but does not propagate via the exception message. + ByteArrayOutputStream captured = new ByteArrayOutputStream(); + PrintStream originalOut = System.out; + PrintStream originalErr = System.err; + PrintStream teeOut = new PrintStream(new TeeOutputStream(originalOut, captured), true); + PrintStream teeErr = new PrintStream(new TeeOutputStream(originalErr, captured), true); - project.setBasedir(buildFile.getParentFile().getAbsolutePath()); - project.setBaseDir(buildFile.getParentFile()); - - project.setUserProperty("ant.file", buildFile.getAbsolutePath()); - if (properties != null) { - for (String k : properties.stringPropertyNames()) { - project.setProperty(k, properties.getProperty(k)); - } - } + DefaultLogger consoleLogger = new DefaultLogger(); + consoleLogger.setErrorPrintStream(teeErr); + consoleLogger.setOutputPrintStream(teeOut); + consoleLogger.setMessageOutputLevel(Project.MSG_INFO); - project.addBuildListener(consoleLogger); + System.setOut(teeOut); + System.setErr(teeErr); - // Capture event for Ant script build start / stop / failure try { - project.fireBuildStarted(); - project.init(); - ProjectHelper projectHelper = ProjectHelper.getProjectHelper(); - - project.addReference("ant.projectHelper", projectHelper); - - projectHelper.parse(project, buildFile); - - // If no target specified then default target will be executed. - String targetToExecute = (target != null && target.trim().length() > 0) ? target.trim() : project.getDefaultTarget(); - project.executeTarget(targetToExecute); - project.fireBuildFinished(null); - success = true; - } catch (BuildException buildException) { - project.fireBuildFinished(buildException); - throw new RuntimeException("!!! Unable to restart the IEHS App !!!", buildException); + // Prepare Ant project + Project project = new Project(); + File buildFile = new File(buildXmlFileFullPath); + + project.setBasedir(buildFile.getParentFile().getAbsolutePath()); + project.setBaseDir(buildFile.getParentFile()); + + project.setUserProperty("ant.file", buildFile.getAbsolutePath()); + if (properties != null) { + for (String k : properties.stringPropertyNames()) { + project.setProperty(k, properties.getProperty(k)); + } + } + + project.addBuildListener(consoleLogger); + + // Capture event for Ant script build start / stop / failure + try { + project.fireBuildStarted(); + project.init(); + ProjectHelper projectHelper = ProjectHelper.getProjectHelper(); + + project.addReference("ant.projectHelper", projectHelper); + + projectHelper.parse(project, buildFile); + + // If no target specified then default target will be executed. + String targetToExecute = (target != null && target.trim().length() > 0) ? target.trim() : project.getDefaultTarget(); + project.executeTarget(targetToExecute); + project.fireBuildFinished(null); + success = true; + } catch (BuildException buildException) { + project.fireBuildFinished(buildException); + teeOut.flush(); + teeErr.flush(); + String detail = extractServerErrorDetail(captured.toString()); + StringBuilder message = new StringBuilder("Ant task failed: ").append(buildException.getMessage()); + if (detail != null) { + message.append(System.lineSeparator()).append(detail); + } + throw new RuntimeException(message.toString(), buildException); + } + } finally { + System.setOut(originalOut); + System.setErr(originalErr); } return success; + } - + /** + * Scans build output for server-reported error markers (HTTP status, response + * message, JSON error body) and returns them joined by newlines, or {@code null} + * if none were found. + */ + static String extractServerErrorDetail(String log) { + if (log == null || log.isEmpty()) { + return null; + } + StringBuilder sb = new StringBuilder(); + String[] lines = log.split("\\r?\\n"); + for (String raw : lines) { + String line = raw.trim(); + if (line.isEmpty()) { + continue; + } + if (line.startsWith("Response message from server is:") + || line.startsWith("Server Detailed Error Message:") + || line.startsWith("Server returned HTTP response code:") + || line.contains("Server returned HTTP response code:")) { + if (sb.length() > 0) { + sb.append(System.lineSeparator()); + } + sb.append(line); + } + } + return sb.length() == 0 ? null : sb.toString(); } /** @@ -88,4 +147,44 @@ private static DefaultLogger getConsoleLogger() { return consoleLogger; } + /** + * OutputStream that writes to two underlying streams. Used to forward Ant + * output to the original console while also retaining a copy for diagnostics. + */ + private static final class TeeOutputStream extends OutputStream { + private final OutputStream a; + private final OutputStream b; + + TeeOutputStream(OutputStream a, OutputStream b) { + this.a = a; + this.b = b; + } + + @Override + public void write(int byteValue) throws IOException { + a.write(byteValue); + b.write(byteValue); + } + + @Override + public void write(byte[] buf, int off, int len) throws IOException { + a.write(buf, off, len); + b.write(buf, off, len); + } + + @Override + public void flush() throws IOException { + a.flush(); + b.flush(); + } + + @Override + public void close() throws IOException { + try { + a.close(); + } finally { + b.close(); + } + } + } } diff --git a/maven/codenameone-maven-plugin/src/test/java/com/codename1/ant/AntExecutorTest.java b/maven/codenameone-maven-plugin/src/test/java/com/codename1/ant/AntExecutorTest.java new file mode 100644 index 0000000000..0197d33856 --- /dev/null +++ b/maven/codenameone-maven-plugin/src/test/java/com/codename1/ant/AntExecutorTest.java @@ -0,0 +1,43 @@ +package com.codename1.ant; + +import org.junit.jupiter.api.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class AntExecutorTest { + + @Test + public void returnsNullWhenNoServerErrorMarkers() { + assertNull(AntExecutor.extractServerErrorDetail(null)); + assertNull(AntExecutor.extractServerErrorDetail("")); + assertNull(AntExecutor.extractServerErrorDetail("Just some build output\nNothing interesting here")); + } + + @Test + public void capturesServerJsonBodyAndStatus() { + String log = "Sending build request to the server, notice that the build might take a while to complete!\n" + + "Sending build to account: shai@codenameone.com\n" + + "Response message from server is: Internal Server Error\n" + + "Server Detailed Error Message: {\"timestamp\":\"2026-05-10T03:43:19.633+00:00\",\"status\":500,\"error\":\"Internal Server Error\",\"path\":\"/appsec/7.0/build/upload\"}\n" + + "java.io.IOException: Server returned HTTP response code: 500 for URL: https://cloud.codenameone.com/appsec/7.0/build/upload\n" + + " at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1700)"; + + String detail = AntExecutor.extractServerErrorDetail(log); + + assertTrue("expected response message line, got: " + detail, + detail.contains("Response message from server is: Internal Server Error")); + assertTrue("expected JSON body, got: " + detail, + detail.contains("Server Detailed Error Message: {\"timestamp\"")); + assertTrue("expected HTTP status line, got: " + detail, + detail.contains("Server returned HTTP response code: 500")); + } + + @Test + public void ignoresUnrelatedLines() { + String log = "Building project\nCompiling sources\nResponse message from server is: OK\nDone"; + String detail = AntExecutor.extractServerErrorDetail(log); + assertEquals("Response message from server is: OK", detail); + } +} From 4d2ad763d0e8acd4b42f64efc960dc8d592ec9a7 Mon Sep 17 00:00:00 2001 From: Shai Almog <67850168+shai-almog@users.noreply.github.com> Date: Sun, 10 May 2026 16:52:51 +0300 Subject: [PATCH 2/2] maven: appease SpotBugs DM_DEFAULT_ENCODING in AntExecutor Pass an explicit UTF-8 charset to the tee PrintStreams and to ByteArrayOutputStream.toString(). Also drop the now-unused getConsoleLogger() helper to silence UPM_UNCALLED_PRIVATE_METHOD. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../java/com/codename1/ant/AntExecutor.java | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/maven/codenameone-maven-plugin/src/main/java/com/codename1/ant/AntExecutor.java b/maven/codenameone-maven-plugin/src/main/java/com/codename1/ant/AntExecutor.java index beef50954d..af46edbdd8 100644 --- a/maven/codenameone-maven-plugin/src/main/java/com/codename1/ant/AntExecutor.java +++ b/maven/codenameone-maven-plugin/src/main/java/com/codename1/ant/AntExecutor.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; +import java.io.UnsupportedEncodingException; import java.util.Properties; import org.apache.tools.ant.BuildException; @@ -42,8 +43,14 @@ public static boolean executeAntTask(String buildXmlFileFullPath, String target, ByteArrayOutputStream captured = new ByteArrayOutputStream(); PrintStream originalOut = System.out; PrintStream originalErr = System.err; - PrintStream teeOut = new PrintStream(new TeeOutputStream(originalOut, captured), true); - PrintStream teeErr = new PrintStream(new TeeOutputStream(originalErr, captured), true); + PrintStream teeOut; + PrintStream teeErr; + try { + teeOut = new PrintStream(new TeeOutputStream(originalOut, captured), true, "UTF-8"); + teeErr = new PrintStream(new TeeOutputStream(originalErr, captured), true, "UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("UTF-8 not supported", e); + } DefaultLogger consoleLogger = new DefaultLogger(); consoleLogger.setErrorPrintStream(teeErr); @@ -89,7 +96,13 @@ public static boolean executeAntTask(String buildXmlFileFullPath, String target, project.fireBuildFinished(buildException); teeOut.flush(); teeErr.flush(); - String detail = extractServerErrorDetail(captured.toString()); + String capturedText; + try { + capturedText = captured.toString("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException("UTF-8 not supported", e); + } + String detail = extractServerErrorDetail(capturedText); StringBuilder message = new StringBuilder("Ant task failed: ").append(buildException.getMessage()); if (detail != null) { message.append(System.lineSeparator()).append(detail); @@ -133,20 +146,6 @@ static String extractServerErrorDetail(String log) { return sb.length() == 0 ? null : sb.toString(); } - /** - * Logger to log output generated while executing ant script in console - * - * @return - */ - private static DefaultLogger getConsoleLogger() { - DefaultLogger consoleLogger = new DefaultLogger(); - consoleLogger.setErrorPrintStream(System.err); - consoleLogger.setOutputPrintStream(System.out); - consoleLogger.setMessageOutputLevel(Project.MSG_INFO); - - return consoleLogger; - } - /** * OutputStream that writes to two underlying streams. Used to forward Ant * output to the original console while also retaining a copy for diagnostics.