Skip to content

Commit 7b664ce

Browse files
bentshermanjorgee
andauthored
Move task error formatting logic into separate class (#6551)
--------- Signed-off-by: Ben Sherman <[email protected]> Co-authored-by: Jorge Ejarque <[email protected]>
1 parent 5b61afe commit 7b664ce

File tree

3 files changed

+567
-194
lines changed

3 files changed

+567
-194
lines changed
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
/*
2+
* Copyright 2013-2024, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package nextflow.processor
17+
18+
import java.lang.reflect.InvocationTargetException
19+
import java.nio.file.FileSystems
20+
import java.nio.file.NoSuchFileException
21+
import java.nio.file.Path
22+
23+
import groovy.transform.CompileStatic
24+
import groovy.transform.Memoized
25+
import groovy.util.logging.Slf4j
26+
import nextflow.exception.FailedGuardException
27+
import nextflow.exception.ProcessEvalException
28+
import nextflow.exception.ShowOnlyExceptionMessage
29+
import nextflow.plugin.Plugins
30+
import nextflow.processor.tip.TaskTipProvider
31+
import nextflow.util.LoggerHelper
32+
/**
33+
* Implement formatting of standard task errors.
34+
*
35+
* @author Paolo Di Tommaso <[email protected]>
36+
*/
37+
@Slf4j
38+
@CompileStatic
39+
class TaskErrorFormatter {
40+
41+
/**
42+
* Format the error message for an eval output error.
43+
*
44+
* @param message
45+
* @param error
46+
* @param task
47+
*/
48+
public List<String> formatCommandError(List<String> message, ProcessEvalException error, TaskRun task) {
49+
// compose a readable error message
50+
message << formatErrorCause(error)
51+
52+
// - print the executed command
53+
message << "Command executed:\n"
54+
for( final line : error.command.stripIndent(true).trim().readLines() )
55+
message << " ${line}".toString()
56+
57+
// - the exit status
58+
message << "\nCommand exit status:\n ${error.status}".toString()
59+
60+
// - the tail of the process stdout
61+
message << "\nCommand output:"
62+
63+
final lines = error.output.readLines()
64+
if( lines.size() == 0 )
65+
message << " (empty)"
66+
for( final line : lines )
67+
message << " ${stripWorkDir(line, task.workDir)}".toString()
68+
69+
if( task.workDir )
70+
message << "\nWork dir:\n ${task.workDirStr}".toString()
71+
72+
return message
73+
}
74+
75+
/**
76+
* Format the error message for a guard error (e.g. `when:`).
77+
*
78+
* @param message
79+
* @param error
80+
* @param task
81+
*/
82+
public List<String> formatGuardError(List<String> message, FailedGuardException error, TaskRun task) {
83+
// compose a readable error message
84+
message << formatErrorCause(error)
85+
86+
if( error.source ) {
87+
message << "\nWhen block:"
88+
for( final line : error.source.stripIndent(true).readLines() )
89+
message << " ${line}".toString()
90+
}
91+
92+
if( task.workDir )
93+
message << "\nWork dir:\n ${task.workDirStr}".toString()
94+
95+
return message
96+
}
97+
98+
/**
99+
* Format the error message for a task error.
100+
*
101+
* @param message
102+
* @param error
103+
* @param task
104+
*/
105+
public List<String> formatTaskError(List<String> message, Throwable error, TaskRun task) {
106+
107+
// prepend a readable error message
108+
message << formatErrorCause( error )
109+
110+
// task with `script:` block
111+
if( task.script ) {
112+
// -- print the executed command
113+
message << "Command executed${task.template ? " [$task.template]": ''}:\n".toString()
114+
for( final line : task.script.stripIndent(true).trim().readLines() )
115+
message << " ${line}".toString()
116+
117+
// -- the exit status
118+
message << "\nCommand exit status:\n ${task.exitStatus != Integer.MAX_VALUE ? task.exitStatus : '-'}".toString()
119+
120+
// -- the tail of the process stdout
121+
message << "\nCommand output:"
122+
final max = 50
123+
final stdoutLines = task.dumpStdout(max)
124+
if( stdoutLines.size() == 0 )
125+
message << " (empty)"
126+
for( final line : stdoutLines )
127+
message << " ${stripWorkDir(line, task.workDir)}".toString()
128+
129+
// -- the tail of the process stderr
130+
final stderrLines = task.dumpStderr(max)
131+
if( stderrLines ) {
132+
message << "\nCommand error:"
133+
for( final line : stderrLines )
134+
message << " ${stripWorkDir(line, task.workDir)}".toString()
135+
}
136+
137+
// -- this is likely a task wrapper issue
138+
else if( task.exitStatus != 0 ) {
139+
final logLines = task.dumpLogFile(max)
140+
if( logLines ) {
141+
message << "\nCommand log:"
142+
for( final line : logLines )
143+
message << " ${stripWorkDir(line, task.workDir)}".toString()
144+
}
145+
}
146+
}
147+
148+
// task with `exec:` block
149+
else if( task.source ) {
150+
message << "Source block:"
151+
for( final line : task.source.stripIndent(true).readLines() )
152+
message << " ${line}".toString()
153+
}
154+
155+
// append work dir if present
156+
if( task.workDir )
157+
message << "\nWork dir:\n ${task.workDirStr}".toString()
158+
159+
// append container image if present
160+
if( task.isContainerEnabled() )
161+
message << "\nContainer:\n ${task.container}".toString()
162+
163+
// append tip
164+
message << suggestTip(message)
165+
166+
return message
167+
}
168+
169+
/**
170+
* Format the cause of an error.
171+
*
172+
* @param error
173+
*/
174+
public String formatErrorCause(Throwable error) {
175+
176+
final result = new StringBuilder()
177+
result.append('\nCaused by:\n')
178+
179+
final message = error instanceof ShowOnlyExceptionMessage || !error.cause
180+
? err0(error)
181+
: err0(error.cause)
182+
183+
for( final line : message.readLines() )
184+
result.append(' ').append(line).append('\n')
185+
186+
return result.append('\n').toString()
187+
}
188+
189+
private static String err0(Throwable e) {
190+
final err = e instanceof InvocationTargetException ? e.targetException : e
191+
192+
if( err instanceof NoSuchFileException ) {
193+
return "No such file or directory: $err.message"
194+
}
195+
196+
if( err instanceof MissingPropertyException ) {
197+
def name = err.property ?: LoggerHelper.getDetailMessage(err)
198+
def result = "No such variable: ${name}"
199+
def details = LoggerHelper.findErrorLine(err)
200+
if( details )
201+
result += " -- Check script '${details[0]}' at line: ${details[1]}"
202+
return result
203+
}
204+
205+
def result = err.message ?: err.toString()
206+
def details = LoggerHelper.findErrorLine(err)
207+
if( details ) {
208+
result += " -- Check script '${details[0]}' at line: ${details[1]}"
209+
}
210+
return result
211+
}
212+
213+
private static String stripWorkDir(String line, Path workDir) {
214+
if( workDir == null )
215+
return line
216+
217+
if( workDir.fileSystem != FileSystems.default )
218+
return line
219+
220+
return workDir ? line.replace(workDir.toString() + '/', '') : line
221+
}
222+
223+
private static String suggestTip(List<String> message) {
224+
try {
225+
return "\nTip: ${getTipProvider().suggestTip(message)}"
226+
}
227+
catch (Exception e) {
228+
log.debug "Unable to get tip for task message: $message", e
229+
return ''
230+
}
231+
}
232+
233+
@Memoized
234+
private static TaskTipProvider getTipProvider() {
235+
final provider = Plugins.getPriorityExtensions(TaskTipProvider).find(it-> it.enabled())
236+
if( !provider )
237+
throw new IllegalStateException("Unable to find any tip provider")
238+
return provider
239+
}
240+
241+
}

0 commit comments

Comments
 (0)