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..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 @@ -1,6 +1,11 @@ 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.io.UnsupportedEncodingException; import java.util.Properties; import org.apache.tools.ant.BuildException; @@ -31,61 +36,154 @@ 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); - - 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)); - } + // 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; + 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); } - project.addBuildListener(consoleLogger); + DefaultLogger consoleLogger = new DefaultLogger(); + consoleLogger.setErrorPrintStream(teeErr); + consoleLogger.setOutputPrintStream(teeOut); + consoleLogger.setMessageOutputLevel(Project.MSG_INFO); + + 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 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); + } + 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(); } /** - * Logger to log output generated while executing ant script in console - * - * @return + * 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 DefaultLogger getConsoleLogger() { - DefaultLogger consoleLogger = new DefaultLogger(); - consoleLogger.setErrorPrintStream(System.err); - consoleLogger.setOutputPrintStream(System.out); - consoleLogger.setMessageOutputLevel(Project.MSG_INFO); + private static final class TeeOutputStream extends OutputStream { + private final OutputStream a; + private final OutputStream b; - return consoleLogger; - } + 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); + } +}