Skip to content

Commit d6123e1

Browse files
authored
Add entrys to steps list for uncaught exceptions (#217)
1 parent 61fcf9a commit d6123e1

File tree

9 files changed

+346
-80
lines changed

9 files changed

+346
-80
lines changed

src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogCard.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class ConsoleLogCard extends React.Component<ConsoleLogCardProps> {
5656
}
5757

5858
getTrucatedLogWarning() {
59-
if (this.props.step.consoleLines && this.props.step.consoleStartByte != 0) {
59+
if (this.props.step.consoleLines && this.props.step.consoleStartByte > 0) {
6060
return (
6161
<Grid container>
6262
<Grid item xs={6} sm className="show-more-console">
@@ -152,7 +152,7 @@ export class ConsoleLogCard extends React.Component<ConsoleLogCardProps> {
152152
<Card
153153
className="step-detail-group"
154154
key={`step-card-${this.props.step.id}`}
155-
style={{ margin: "5px", padding: "5px" }}
155+
style={{ marginBottom: "5px" }}
156156
>
157157
<CardActionArea
158158
onClick={this.handleStepToggle}
@@ -173,6 +173,7 @@ export class ConsoleLogCard extends React.Component<ConsoleLogCardProps> {
173173
container
174174
xs={16}
175175
sx={{ display: "block", margin: "auto" }}
176+
width="80%"
176177
>
177178
<Typography
178179
className="log-card-header"
@@ -226,7 +227,7 @@ export class ConsoleLogCard extends React.Component<ConsoleLogCardProps> {
226227
>
227228
<ExpandMoreIcon
228229
key={`step-expand-icon-${this.props.step.id}`}
229-
className="svg-icon"
230+
className="svg-icon svg-icon--expand"
230231
/>
231232
</ExpandMore>
232233
</Grid>

src/main/frontend/pipeline-console-view/pipeline-console/main/pipeline-console.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ pre.console-output-line {
246246
color: currentColor !important;
247247
}
248248

249+
.svg-icon--expand {
250+
color: var(--step-text-color);
251+
}
252+
249253
// Console styling
250254
a.console-line-number {
251255
text-align: right;

src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java

Lines changed: 68 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,12 @@
44
import hudson.console.AnnotatedLargeText;
55
import hudson.util.HttpResponses;
66
import io.jenkins.plugins.pipelinegraphview.utils.AbstractPipelineViewAction;
7+
import io.jenkins.plugins.pipelinegraphview.utils.PipelineNodeUtil;
78
import io.jenkins.plugins.pipelinegraphview.utils.PipelineStepApi;
89
import io.jenkins.plugins.pipelinegraphview.utils.PipelineStepList;
910
import java.io.IOException;
10-
import java.io.Writer;
1111
import java.util.HashMap;
1212
import net.sf.json.JSONObject;
13-
import org.apache.commons.io.output.StringBuilderWriter;
14-
import org.jenkinsci.plugins.workflow.actions.LogAction;
1513
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
1614
import org.jenkinsci.plugins.workflow.graph.FlowNode;
1715
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
@@ -97,52 +95,65 @@ public HttpResponse getAllSteps(StaplerRequest req) throws IOException {
9795
*/
9896
@GET
9997
@WebMethod(name = "consoleOutput")
100-
public HttpResponse getConsolOutput(StaplerRequest req) throws IOException {
98+
public HttpResponse getConsoleOutput(StaplerRequest req) throws IOException {
10199
String nodeId = req.getParameter("nodeId");
102100
if (nodeId == null) {
103101
logger.error("'consoleJson' was not passed 'nodeId'.");
104102
return HttpResponses.errorJSON("Error getting console json");
105103
}
106-
// startByte to start getting data from. If negative will startByte from end of string with
107-
// LOG_THRESHOLD.
108-
Long startByte = parseIntWithDefault(req.getParameter("startByte"), -LOG_THRESHOLD);
109104
logger.debug("getConsoleOutput was passed node id '" + nodeId + "'.");
110-
Writer stringWriter = new StringBuilderWriter();
111-
AnnotatedLargeText<? extends FlowNode> logText = getLogForNode(nodeId);
112-
HashMap<String, Object> response = new HashMap<String, Object>();
105+
106+
Long startByte = 0L;
113107
long endByte = 0L;
114108
long textLength = 0L;
115109
String text = "";
116-
if (logText != null) {
117-
textLength = logText.length();
118-
// postitive startByte
119-
if (startByte > textLength) {
120-
// Avoid resource leak.
121-
stringWriter.close();
122-
logger.error("consoleJson - user requested startByte larger than console output.");
123-
return HttpResponses.errorJSON("startByte too large.");
124-
}
125-
// if startByte is negative make sure we don't try and get a byte before 0.
126-
if (startByte < 0) {
127-
logger.info(
128-
"consoleJson - requested negative startByte '" + Long.toString(startByte) + "'.");
129-
startByte = textLength + startByte;
110+
// If this is an exception, return the exception text (inc. stacktrace).
111+
if (isUnhandledException(nodeId)) {
112+
// Set logText to exception text. This is a little hacky - maybe it would be better update the
113+
// frontend to handle steps and exceptions differently?
114+
text = getNodeExceptionText(nodeId);
115+
endByte = text.length();
116+
} else {
117+
// This will be a step, so return it's log output.
118+
// startByte to start getting data from. If negative will startByte from end of string with
119+
// LOG_THRESHOLD.
120+
startByte = parseIntWithDefault(req.getParameter("startByte"), -LOG_THRESHOLD);
121+
122+
AnnotatedLargeText<? extends FlowNode> logText = getLogForNode(nodeId);
123+
124+
if (logText != null) {
125+
textLength = logText.length();
126+
// postitive startByte
127+
if (startByte > textLength) {
128+
// Avoid resource leak.
129+
logger.error("consoleJson - user requested startByte larger than console output.");
130+
return HttpResponses.errorJSON("startByte too large.");
131+
}
132+
// if startByte is negative make sure we don't try and get a byte before 0.
130133
if (startByte < 0) {
131-
logger.info(
132-
"consoleJson - requested negative startByte '"
133-
+ Long.toString(startByte)
134-
+ "' out of bounds, setting to 0.");
135-
startByte = 0L;
134+
logger.debug(
135+
"consoleJson - requested negative startByte '" + Long.toString(startByte) + "'.");
136+
startByte = textLength + startByte;
137+
if (startByte < 0) {
138+
logger.debug(
139+
"consoleJson - requested negative startByte '"
140+
+ Long.toString(startByte)
141+
+ "' out of bounds, setting to 0.");
142+
startByte = 0L;
143+
}
136144
}
145+
146+
logger.debug(
147+
"Returning '"
148+
+ Long.toString(textLength - startByte)
149+
+ "' bytes from 'getConsoleOutput'.");
150+
} else {
151+
// If there is no text then set set startByte to 0 - as we have read from the start, there
152+
// is just nothing there.
153+
startByte = 0L;
137154
}
138-
// NOTE: This returns the total length of the console log, not the received bytes.
139-
endByte = logText.writeHtmlTo(startByte, stringWriter);
140-
text = stringWriter.toString();
141-
logger.info(
142-
"Returning '"
143-
+ Long.toString(textLength - startByte)
144-
+ "' bytes from 'getConsolOutput'.");
145155
}
156+
HashMap<String, Object> response = new HashMap<String, Object>();
146157
response.put("text", text);
147158
response.put("startByte", startByte);
148159
response.put("endByte", endByte);
@@ -152,25 +163,35 @@ public HttpResponse getConsolOutput(StaplerRequest req) throws IOException {
152163
private AnnotatedLargeText<? extends FlowNode> getLogForNode(String nodeId) throws IOException {
153164
FlowExecution execution = target.getExecution();
154165
if (execution != null) {
155-
logger.debug("getConsoleOutput found execution.");
156-
FlowNode node = execution.getNode(nodeId);
157-
if (node != null) {
158-
logger.debug("getConsoleOutput found node.");
159-
LogAction log = node.getAction(LogAction.class);
160-
if (log != null) {
161-
return log.getLogText();
162-
}
163-
}
166+
logger.debug("getLogForNode found execution.");
167+
PipelineNodeUtil.getLogText(execution.getNode(nodeId));
164168
}
165169
return null;
166170
}
167171

172+
private String getNodeExceptionText(String nodeId) throws IOException {
173+
FlowExecution execution = target.getExecution();
174+
if (execution != null) {
175+
logger.debug("getNodeException found execution.");
176+
return PipelineNodeUtil.getExceptionText(execution.getNode(nodeId));
177+
}
178+
return null;
179+
}
180+
181+
private boolean isUnhandledException(String nodeId) throws IOException {
182+
FlowExecution execution = target.getExecution();
183+
if (execution != null) {
184+
return PipelineNodeUtil.isUnhandledException(execution.getNode(nodeId));
185+
}
186+
return false;
187+
}
188+
168189
private static long parseIntWithDefault(String s, long default_value) {
169190
try {
170-
logger.info("Parsing user provided value of '" + s + "'");
191+
logger.debug("Parsing user provided value of '" + s + "'");
171192
return Long.parseLong(s);
172193
} catch (NumberFormatException e) {
173-
logger.info("Using default value of '" + default_value + "'");
194+
logger.debug("Using default value of '" + default_value + "'");
174195
return default_value;
175196
}
176197
}

src/main/java/io/jenkins/plugins/pipelinegraphview/utils/FlowNodeWrapper.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ public boolean probablySameNode(@Nullable FlowNodeWrapper that) {
5858
public enum NodeType {
5959
STAGE,
6060
PARALLEL,
61-
STEP
61+
STEP,
62+
UNHANDLED_EXCEPTION,
6263
}
6364

6465
private final FlowNode node;
@@ -122,6 +123,8 @@ private static NodeType getNodeType(FlowNode node) {
122123
return NodeType.PARALLEL;
123124
} else if (node instanceof AtomNode) {
124125
return NodeType.STEP;
126+
} else if (PipelineNodeUtil.isUnhandledException(node)) {
127+
return NodeType.UNHANDLED_EXCEPTION;
125128
}
126129
throw new IllegalArgumentException(
127130
String.format("Unknown FlowNode %s, type: %s", node.getId(), node.getClass()));
@@ -290,4 +293,8 @@ public String getArgumentsAsString() {
290293
public boolean isSynthetic() {
291294
return PipelineNodeUtil.isSyntheticStage(node);
292295
}
296+
297+
public boolean isUnhandledException() {
298+
return PipelineNodeUtil.isUnhandledException(node);
299+
}
293300
}

src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineNodeUtil.java

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,21 @@
44
import edu.umd.cs.findbugs.annotations.CheckForNull;
55
import edu.umd.cs.findbugs.annotations.NonNull;
66
import edu.umd.cs.findbugs.annotations.Nullable;
7+
import edu.umd.cs.findbugs.annotations.SuppressWarnings;
8+
import hudson.AbortException;
9+
import hudson.console.AnnotatedLargeText;
710
import hudson.model.Action;
811
import hudson.model.Queue;
912
import hudson.model.queue.CauseOfBlockage;
13+
import java.io.IOException;
14+
import java.io.Writer;
15+
import java.util.Arrays;
16+
import java.util.stream.Collectors;
17+
import org.apache.commons.io.output.StringBuilderWriter;
1018
import org.jenkinsci.plugins.pipeline.StageStatus;
1119
import org.jenkinsci.plugins.pipeline.SyntheticStage;
1220
import org.jenkinsci.plugins.workflow.actions.ArgumentsAction;
21+
import org.jenkinsci.plugins.workflow.actions.ErrorAction;
1322
import org.jenkinsci.plugins.workflow.actions.LabelAction;
1423
import org.jenkinsci.plugins.workflow.actions.LogAction;
1524
import org.jenkinsci.plugins.workflow.actions.QueueItemAction;
@@ -112,6 +121,10 @@ public static boolean isParallelBranch(@Nullable FlowNode node) {
112121
&& node.getAction(ThreadNameAction.class) != null;
113122
}
114123

124+
public static boolean isUnhandledException(@Nullable FlowNode node) {
125+
return node != null && node.getAction(ErrorAction.class) != null;
126+
}
127+
115128
public static String getArgumentsAsString(@Nullable FlowNode node) {
116129
if (node != null) {
117130
return ArgumentsAction.getStepArgumentsAsString(node);
@@ -209,4 +222,78 @@ public static boolean isAgentStart(@Nullable FlowNode node) {
209222

210223
return false;
211224
}
225+
226+
/* Get the AnnotatedLargeText for a given node.
227+
* @param node a possibly null {@link FlowNode}
228+
* @return The AnnotatedLargeText object representing the log text for this node, or null.
229+
*/
230+
public static AnnotatedLargeText<? extends FlowNode> getLogText(@Nullable FlowNode node) {
231+
if (node != null) {
232+
LogAction logAction = node.getAction(LogAction.class);
233+
if (logAction != null) {
234+
return logAction.getLogText();
235+
}
236+
}
237+
return null;
238+
}
239+
240+
/* Get the generated log text for a given node.
241+
* @param log The AnnotatedLargeText object for a given node.
242+
* @return The AnnotatedLargeText object representing the log text for this node, or null.
243+
*/
244+
public static String convertLogToString(AnnotatedLargeText<? extends FlowNode> log)
245+
throws IOException {
246+
return convertLogToString(log, 0L);
247+
}
248+
249+
/* Get the generated log text for a given node.
250+
* @param log The AnnotatedLargeText object for a given node.
251+
* @param startByte The byte to start parsing from.
252+
* @return The AnnotatedLargeText object representing the log text for this node, or null.
253+
*/
254+
@SuppressWarnings("RV_RETURN_VALUE_IGNORED")
255+
public static String convertLogToString(
256+
AnnotatedLargeText<? extends FlowNode> log, Long startByte) throws IOException {
257+
Writer stringWriter = new StringBuilderWriter();
258+
// NOTE: This returns the total length of the console log, not the received bytes.
259+
260+
log.writeHtmlTo(startByte, stringWriter);
261+
return stringWriter.toString();
262+
}
263+
264+
/* The exception text from aa FlowNode.
265+
* @param node a possibly null {@link FlowNode}
266+
* @return A string representing the exception thrown from this node, or null.
267+
*/
268+
public static String getExceptionText(@Nullable FlowNode node) {
269+
if (node != null) {
270+
String log = null;
271+
ErrorAction error = node.getAction(ErrorAction.class);
272+
if (error != null) {
273+
Throwable exception = error.getError();
274+
if (PipelineNodeUtil.isJenkinsFailureException(exception)) {
275+
// If this is a Jenkins exception to mark a build failure then only return the mesage -
276+
// the stack trace is unimportant as the message will be attached to the step.
277+
log = exception.getMessage();
278+
} else {
279+
// If this is not a Jenkins failure exception, then we should print everything.
280+
log = "Found unhandled " + exception.getClass().getName() + " exception:\n";
281+
log += exception.getMessage() + "\n\t";
282+
log +=
283+
Arrays.stream(exception.getStackTrace())
284+
.map(s -> s.toString())
285+
.collect(Collectors.joining("\n\t"));
286+
}
287+
}
288+
return log;
289+
}
290+
return null;
291+
}
292+
293+
public static boolean isJenkinsFailureException(Throwable exception) {
294+
if (exception instanceof AbortException) {
295+
return true;
296+
}
297+
return false;
298+
}
212299
}

src/main/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStepApi.java

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,22 +25,25 @@ private List<PipelineStep> parseSteps(List<FlowNodeWrapper> stepNodes, String st
2525
stepNodes.stream()
2626
.map(
2727
flowNodeWrapper -> {
28-
String state = flowNodeWrapper.getStatus().getResult().name();
29-
// TODO: Why do we do this? Seems like it will return uppercase for some states
30-
// and lowercase for others?
28+
String state =
29+
flowNodeWrapper.getStatus().getResult().name().toLowerCase(Locale.ROOT);
3130
if (flowNodeWrapper.getStatus().getState() != BlueRun.BlueRunState.FINISHED) {
3231
state = flowNodeWrapper.getStatus().getState().name().toLowerCase(Locale.ROOT);
3332
}
3433
String displayName = flowNodeWrapper.getDisplayName();
35-
String stepArguments = flowNodeWrapper.getArgumentsAsString();
36-
if (stepArguments != null && !stepArguments.isEmpty()) {
37-
displayName = stepArguments + " - " + displayName;
38-
}
3934

40-
// Use the step label as the displayName if set
41-
String labelDisplayName = flowNodeWrapper.getLabelDisplayName();
42-
if (labelDisplayName != null && !labelDisplayName.isEmpty()) {
43-
displayName = labelDisplayName;
35+
if (flowNodeWrapper.getType() == FlowNodeWrapper.NodeType.UNHANDLED_EXCEPTION) {
36+
displayName = "Pipeline error";
37+
} else {
38+
String stepArguments = flowNodeWrapper.getArgumentsAsString();
39+
if (stepArguments != null && !stepArguments.isEmpty()) {
40+
displayName = stepArguments + " - " + displayName;
41+
}
42+
// Use the step label as the displayName if set
43+
String labelDisplayName = flowNodeWrapper.getLabelDisplayName();
44+
if (labelDisplayName != null && !labelDisplayName.isEmpty()) {
45+
displayName = labelDisplayName;
46+
}
4447
}
4548
// Remove non-printable chars (e.g. ANSI color codes).
4649
logger.debug("DisplayName Before: '" + displayName + "'.");

0 commit comments

Comments
 (0)