Skip to content

Commit 16e63cc

Browse files
shai-almogclaude
andauthored
maven: surface real Ant/server error in AntExecutor failures (#4911)
* 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) <noreply@anthropic.com> * 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) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3a6fcbf commit 16e63cc

2 files changed

Lines changed: 183 additions & 42 deletions

File tree

Lines changed: 140 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package com.codename1.ant;
22

3+
import java.io.ByteArrayOutputStream;
34
import java.io.File;
5+
import java.io.IOException;
6+
import java.io.OutputStream;
7+
import java.io.PrintStream;
8+
import java.io.UnsupportedEncodingException;
49
import java.util.Properties;
510

611
import org.apache.tools.ant.BuildException;
@@ -31,61 +36,154 @@ public static boolean executeAntTask(String buildXmlFileFullPath) {
3136
public static boolean executeAntTask(String buildXmlFileFullPath, String target, Properties properties) {
3237

3338
boolean success = false;
34-
DefaultLogger consoleLogger = getConsoleLogger();
3539

36-
// Prepare Ant project
37-
Project project = new Project();
38-
File buildFile = new File(buildXmlFileFullPath);
39-
40-
project.setBasedir(buildFile.getParentFile().getAbsolutePath());
41-
project.setBaseDir(buildFile.getParentFile());
42-
43-
project.setUserProperty("ant.file", buildFile.getAbsolutePath());
44-
if (properties != null) {
45-
for (String k : properties.stringPropertyNames()) {
46-
project.setProperty(k, properties.getProperty(k));
47-
}
40+
// Tee stdout/stderr so that, on failure, we can recover server-reported
41+
// error details (such as the JSON body returned by the build server) that
42+
// the build client prints but does not propagate via the exception message.
43+
ByteArrayOutputStream captured = new ByteArrayOutputStream();
44+
PrintStream originalOut = System.out;
45+
PrintStream originalErr = System.err;
46+
PrintStream teeOut;
47+
PrintStream teeErr;
48+
try {
49+
teeOut = new PrintStream(new TeeOutputStream(originalOut, captured), true, "UTF-8");
50+
teeErr = new PrintStream(new TeeOutputStream(originalErr, captured), true, "UTF-8");
51+
} catch (UnsupportedEncodingException e) {
52+
throw new IllegalStateException("UTF-8 not supported", e);
4853
}
4954

50-
project.addBuildListener(consoleLogger);
55+
DefaultLogger consoleLogger = new DefaultLogger();
56+
consoleLogger.setErrorPrintStream(teeErr);
57+
consoleLogger.setOutputPrintStream(teeOut);
58+
consoleLogger.setMessageOutputLevel(Project.MSG_INFO);
59+
60+
System.setOut(teeOut);
61+
System.setErr(teeErr);
5162

52-
// Capture event for Ant script build start / stop / failure
5363
try {
54-
project.fireBuildStarted();
55-
project.init();
56-
ProjectHelper projectHelper = ProjectHelper.getProjectHelper();
57-
58-
project.addReference("ant.projectHelper", projectHelper);
59-
60-
projectHelper.parse(project, buildFile);
61-
62-
// If no target specified then default target will be executed.
63-
String targetToExecute = (target != null && target.trim().length() > 0) ? target.trim() : project.getDefaultTarget();
64-
project.executeTarget(targetToExecute);
65-
project.fireBuildFinished(null);
66-
success = true;
67-
} catch (BuildException buildException) {
68-
project.fireBuildFinished(buildException);
69-
throw new RuntimeException("!!! Unable to restart the IEHS App !!!", buildException);
64+
// Prepare Ant project
65+
Project project = new Project();
66+
File buildFile = new File(buildXmlFileFullPath);
67+
68+
project.setBasedir(buildFile.getParentFile().getAbsolutePath());
69+
project.setBaseDir(buildFile.getParentFile());
70+
71+
project.setUserProperty("ant.file", buildFile.getAbsolutePath());
72+
if (properties != null) {
73+
for (String k : properties.stringPropertyNames()) {
74+
project.setProperty(k, properties.getProperty(k));
75+
}
76+
}
77+
78+
project.addBuildListener(consoleLogger);
79+
80+
// Capture event for Ant script build start / stop / failure
81+
try {
82+
project.fireBuildStarted();
83+
project.init();
84+
ProjectHelper projectHelper = ProjectHelper.getProjectHelper();
85+
86+
project.addReference("ant.projectHelper", projectHelper);
87+
88+
projectHelper.parse(project, buildFile);
89+
90+
// If no target specified then default target will be executed.
91+
String targetToExecute = (target != null && target.trim().length() > 0) ? target.trim() : project.getDefaultTarget();
92+
project.executeTarget(targetToExecute);
93+
project.fireBuildFinished(null);
94+
success = true;
95+
} catch (BuildException buildException) {
96+
project.fireBuildFinished(buildException);
97+
teeOut.flush();
98+
teeErr.flush();
99+
String capturedText;
100+
try {
101+
capturedText = captured.toString("UTF-8");
102+
} catch (UnsupportedEncodingException e) {
103+
throw new IllegalStateException("UTF-8 not supported", e);
104+
}
105+
String detail = extractServerErrorDetail(capturedText);
106+
StringBuilder message = new StringBuilder("Ant task failed: ").append(buildException.getMessage());
107+
if (detail != null) {
108+
message.append(System.lineSeparator()).append(detail);
109+
}
110+
throw new RuntimeException(message.toString(), buildException);
111+
}
112+
} finally {
113+
System.setOut(originalOut);
114+
System.setErr(originalErr);
70115
}
71116

72117
return success;
118+
}
73119

74-
120+
/**
121+
* Scans build output for server-reported error markers (HTTP status, response
122+
* message, JSON error body) and returns them joined by newlines, or {@code null}
123+
* if none were found.
124+
*/
125+
static String extractServerErrorDetail(String log) {
126+
if (log == null || log.isEmpty()) {
127+
return null;
128+
}
129+
StringBuilder sb = new StringBuilder();
130+
String[] lines = log.split("\\r?\\n");
131+
for (String raw : lines) {
132+
String line = raw.trim();
133+
if (line.isEmpty()) {
134+
continue;
135+
}
136+
if (line.startsWith("Response message from server is:")
137+
|| line.startsWith("Server Detailed Error Message:")
138+
|| line.startsWith("Server returned HTTP response code:")
139+
|| line.contains("Server returned HTTP response code:")) {
140+
if (sb.length() > 0) {
141+
sb.append(System.lineSeparator());
142+
}
143+
sb.append(line);
144+
}
145+
}
146+
return sb.length() == 0 ? null : sb.toString();
75147
}
76148

77149
/**
78-
* Logger to log output generated while executing ant script in console
79-
*
80-
* @return
150+
* OutputStream that writes to two underlying streams. Used to forward Ant
151+
* output to the original console while also retaining a copy for diagnostics.
81152
*/
82-
private static DefaultLogger getConsoleLogger() {
83-
DefaultLogger consoleLogger = new DefaultLogger();
84-
consoleLogger.setErrorPrintStream(System.err);
85-
consoleLogger.setOutputPrintStream(System.out);
86-
consoleLogger.setMessageOutputLevel(Project.MSG_INFO);
153+
private static final class TeeOutputStream extends OutputStream {
154+
private final OutputStream a;
155+
private final OutputStream b;
87156

88-
return consoleLogger;
89-
}
157+
TeeOutputStream(OutputStream a, OutputStream b) {
158+
this.a = a;
159+
this.b = b;
160+
}
161+
162+
@Override
163+
public void write(int byteValue) throws IOException {
164+
a.write(byteValue);
165+
b.write(byteValue);
166+
}
167+
168+
@Override
169+
public void write(byte[] buf, int off, int len) throws IOException {
170+
a.write(buf, off, len);
171+
b.write(buf, off, len);
172+
}
173+
174+
@Override
175+
public void flush() throws IOException {
176+
a.flush();
177+
b.flush();
178+
}
90179

180+
@Override
181+
public void close() throws IOException {
182+
try {
183+
a.close();
184+
} finally {
185+
b.close();
186+
}
187+
}
188+
}
91189
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.codename1.ant;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.junit.Assert.assertEquals;
6+
import static org.junit.Assert.assertNull;
7+
import static org.junit.Assert.assertTrue;
8+
9+
public class AntExecutorTest {
10+
11+
@Test
12+
public void returnsNullWhenNoServerErrorMarkers() {
13+
assertNull(AntExecutor.extractServerErrorDetail(null));
14+
assertNull(AntExecutor.extractServerErrorDetail(""));
15+
assertNull(AntExecutor.extractServerErrorDetail("Just some build output\nNothing interesting here"));
16+
}
17+
18+
@Test
19+
public void capturesServerJsonBodyAndStatus() {
20+
String log = "Sending build request to the server, notice that the build might take a while to complete!\n"
21+
+ "Sending build to account: shai@codenameone.com\n"
22+
+ "Response message from server is: Internal Server Error\n"
23+
+ "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"
24+
+ "java.io.IOException: Server returned HTTP response code: 500 for URL: https://cloud.codenameone.com/appsec/7.0/build/upload\n"
25+
+ " at java.base/sun.net.www.protocol.http.HttpURLConnection.getInputStream0(HttpURLConnection.java:1700)";
26+
27+
String detail = AntExecutor.extractServerErrorDetail(log);
28+
29+
assertTrue("expected response message line, got: " + detail,
30+
detail.contains("Response message from server is: Internal Server Error"));
31+
assertTrue("expected JSON body, got: " + detail,
32+
detail.contains("Server Detailed Error Message: {\"timestamp\""));
33+
assertTrue("expected HTTP status line, got: " + detail,
34+
detail.contains("Server returned HTTP response code: 500"));
35+
}
36+
37+
@Test
38+
public void ignoresUnrelatedLines() {
39+
String log = "Building project\nCompiling sources\nResponse message from server is: OK\nDone";
40+
String detail = AntExecutor.extractServerErrorDetail(log);
41+
assertEquals("Response message from server is: OK", detail);
42+
}
43+
}

0 commit comments

Comments
 (0)