Skip to content

Commit 3daac77

Browse files
committed
always store command output to temporary file in run_shell_cmd
1 parent ece71c2 commit 3daac77

File tree

2 files changed

+66
-60
lines changed

2 files changed

+66
-60
lines changed

easybuild/tools/run.py

Lines changed: 48 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,16 @@
7676
]
7777

7878

79-
RunShellCmdResult = namedtuple('RunShellCmdResult', ('cmd', 'exit_code', 'output', 'stderr', 'work_dir'))
79+
RunShellCmdResult = namedtuple('RunShellCmdResult', ('cmd', 'exit_code', 'output', 'stderr', 'work_dir',
80+
'out_file', 'err_file'))
8081

8182

8283
class RunShellCmdError(BaseException):
8384

84-
def __init__(self, cmd, exit_code, work_dir, output, stderr, caller_info, *args, **kwargs):
85+
def __init__(self, cmd_result, caller_info, *args, **kwargs):
8586
"""Constructor for RunShellCmdError."""
86-
self.cmd = cmd
87-
self.cmd_name = cmd.split(' ')[0]
88-
self.exit_code = exit_code
89-
self.work_dir = work_dir
90-
self.output = output
91-
self.stderr = stderr
87+
self.cmd_result = cmd_result
88+
self.cmd_name = cmd_result.cmd.split(' ')[0]
9289
self.caller_info = caller_info
9390

9491
msg = f"Shell command '{self.cmd_name}' failed!"
@@ -105,25 +102,17 @@ def pad_4_spaces(msg):
105102
error_info = [
106103
'',
107104
"ERROR: Shell command failed!",
108-
pad_4_spaces(f"full command -> {self.cmd}"),
109-
pad_4_spaces(f"exit code -> {self.exit_code}"),
110-
pad_4_spaces(f"working directory -> {self.work_dir}"),
105+
pad_4_spaces(f"full command -> {self.cmd_result.cmd}"),
106+
pad_4_spaces(f"exit code -> {self.cmd_result.exit_code}"),
107+
pad_4_spaces(f"working directory -> {self.cmd_result.work_dir}"),
111108
]
112109

113-
tmpdir = tempfile.mkdtemp(prefix='shell-cmd-error-')
114-
output_fp = os.path.join(tmpdir, f"{self.cmd_name}.out")
115-
with open(output_fp, 'w') as fp:
116-
fp.write(self.output or '')
117-
118-
if self.stderr is None:
119-
error_info.append(pad_4_spaces(f"output (stdout + stderr) -> {output_fp}"))
110+
if self.cmd_result.stderr is None:
111+
error_info.append(pad_4_spaces(f"output (stdout + stderr) -> {self.cmd_result.out_file}"))
120112
else:
121-
stderr_fp = os.path.join(tmpdir, f"{self.cmd_name}.err")
122-
with open(stderr_fp, 'w') as fp:
123-
fp.write(self.stderr)
124113
error_info.extend([
125-
pad_4_spaces(f"output (stdout) -> {output_fp}"),
126-
pad_4_spaces(f"error/warnings (stderr) -> {stderr_fp}"),
114+
pad_4_spaces(f"output (stdout) -> {self.cmd_result.out_file}"),
115+
pad_4_spaces(f"error/warnings (stderr) -> {self.cmd_result.err_file}"),
127116
])
128117

129118
caller_file_name, caller_line_nr, caller_function_name = self.caller_info
@@ -136,9 +125,9 @@ def pad_4_spaces(msg):
136125
sys.stderr.write('\n'.join(error_info) + '\n')
137126

138127

139-
def raise_run_shell_cmd_error(cmd, exit_code, work_dir, output, stderr):
128+
def raise_run_shell_cmd_error(cmd_res):
140129
"""
141-
Raise RunShellCmdError for failing shell command, after collecting additional caller info
130+
Raise RunShellCmdError for failed shell command, after collecting additional caller info
142131
"""
143132

144133
# figure out where failing command was run
@@ -150,7 +139,7 @@ def raise_run_shell_cmd_error(cmd, exit_code, work_dir, output, stderr):
150139
frameinfo = inspect.getouterframes(inspect.currentframe())[3]
151140
caller_info = (frameinfo.filename, frameinfo.lineno, frameinfo.function)
152141

153-
raise RunShellCmdError(cmd, exit_code, work_dir, output, stderr, caller_info)
142+
raise RunShellCmdError(cmd_res, caller_info)
154143

155144

156145
def run_cmd_cache(func):
@@ -234,16 +223,18 @@ def to_cmd_str(cmd):
234223
if work_dir is None:
235224
work_dir = os.getcwd()
236225

237-
# temporary output file for command output, if requested
238-
if output_file or not hidden:
239-
# collect output of running command in temporary log file, if desired
240-
fd, cmd_out_fp = tempfile.mkstemp(suffix='.log', prefix='easybuild-run-')
241-
os.close(fd)
242-
_log.info(f'run_cmd: Output of "{cmd}" will be logged to {cmd_out_fp}')
243-
else:
244-
cmd_out_fp = None
245-
246226
cmd_str = to_cmd_str(cmd)
227+
cmd_name = cmd_str.split(' ')[0]
228+
229+
# temporary output file(s) for command output
230+
tmpdir = tempfile.mkdtemp(prefix='run-shell-cmd-')
231+
cmd_out_fp = os.path.join(tmpdir, f'{cmd_name}.out')
232+
_log.info(f'run_cmd: Output of "{cmd_str}" will be logged to {cmd_out_fp}')
233+
if split_stderr:
234+
cmd_err_fp = os.path.join(tmpdir, f'{cmd_name}.err')
235+
_log.info(f'run_cmd: Errors and warnings of "{cmd_str}" will be logged to {cmd_err_fp}')
236+
else:
237+
cmd_err_fp = None
247238

248239
# early exit in 'dry run' mode, after printing the command that would be run (unless 'hidden' is enabled)
249240
if not in_dry_run and build_option('extended_dry_run'):
@@ -253,11 +244,12 @@ def to_cmd_str(cmd):
253244
msg += f" (in {work_dir})"
254245
dry_run_msg(msg, silent=silent)
255246

256-
return RunShellCmdResult(cmd=cmd_str, exit_code=0, output='', stderr=None, work_dir=work_dir)
247+
return RunShellCmdResult(cmd=cmd_str, exit_code=0, output='', stderr=None, work_dir=work_dir,
248+
out_file=cmd_out_fp, err_file=cmd_err_fp)
257249

258250
start_time = datetime.now()
259251
if not hidden:
260-
cmd_trace_msg(cmd_str, start_time, work_dir, stdin, cmd_out_fp)
252+
cmd_trace_msg(cmd_str, start_time, work_dir, stdin, cmd_out_fp, cmd_err_fp)
261253

262254
if stdin:
263255
# 'input' value fed to subprocess.run must be a byte sequence
@@ -286,7 +278,18 @@ def to_cmd_str(cmd):
286278
output = proc.stdout.decode('utf-8', 'ignore')
287279
stderr = proc.stderr.decode('utf-8', 'ignore') if split_stderr else None
288280

289-
res = RunShellCmdResult(cmd=cmd_str, exit_code=proc.returncode, output=output, stderr=stderr, work_dir=work_dir)
281+
# store command output to temporary file(s)
282+
try:
283+
with open(cmd_out_fp, 'w') as fp:
284+
fp.write(output)
285+
if split_stderr:
286+
with open(cmd_err_fp, 'w') as fp:
287+
fp.write(stderr)
288+
except IOError as err:
289+
raise EasyBuildError(f"Failed to dump command output to temporary file: {err}")
290+
291+
res = RunShellCmdResult(cmd=cmd_str, exit_code=proc.returncode, output=output, stderr=stderr, work_dir=work_dir,
292+
out_file=cmd_out_fp, err_file=cmd_err_fp)
290293

291294
# always log command output
292295
cmd_name = cmd_str.split(' ')[0]
@@ -301,7 +304,7 @@ def to_cmd_str(cmd):
301304
else:
302305
_log.warning(f"Shell command FAILED (exit code {res.exit_code}, see output above): {cmd_str}")
303306
if fail_on_error:
304-
raise_run_shell_cmd_error(res.cmd, res.exit_code, res.work_dir, output=res.output, stderr=res.stderr)
307+
raise_run_shell_cmd_error(res)
305308

306309
if with_hooks:
307310
run_hook_kwargs = {
@@ -319,15 +322,16 @@ def to_cmd_str(cmd):
319322
return res
320323

321324

322-
def cmd_trace_msg(cmd, start_time, work_dir, stdin, cmd_out_fp):
325+
def cmd_trace_msg(cmd, start_time, work_dir, stdin, cmd_out_fp, cmd_err_fp):
323326
"""
324327
Helper function to construct and print trace message for command being run
325328
326329
:param cmd: command being run
327330
:param start_time: datetime object indicating when command was started
328331
:param work_dir: path of working directory in which command is run
329332
:param stdin: stdin input value for command
330-
:param cmd_out_fp: path to output log file for command
333+
:param cmd_out_fp: path to output file for command
334+
:param cmd_err_fp: path to errors/warnings output file for command
331335
"""
332336
start_time = start_time.strftime('%Y-%m-%d %H:%M:%S')
333337

@@ -338,8 +342,9 @@ def cmd_trace_msg(cmd, start_time, work_dir, stdin, cmd_out_fp):
338342
]
339343
if stdin:
340344
lines.append(f"\t[input: {stdin}]")
341-
if cmd_out_fp:
342-
lines.append(f"\t[output logged in {cmd_out_fp}]")
345+
lines.append(f"\t[output saved to {cmd_out_fp}]")
346+
if cmd_err_fp:
347+
lines.append(f"\t[errors/warnings saved to {cmd_err_fp}]")
343348

344349
lines.append('\t' + cmd)
345350

test/framework/run.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -333,12 +333,12 @@ def handler(signum, _):
333333
self.assertFalse("This should never be reached, RunShellCmdError should occur!")
334334
except RunShellCmdError as err:
335335
self.assertEqual(str(err), "Shell command 'kill' failed!")
336-
self.assertEqual(err.cmd, "kill -9 $$")
336+
self.assertEqual(err.cmd_result.cmd, "kill -9 $$")
337337
self.assertEqual(err.cmd_name, 'kill')
338-
self.assertEqual(err.exit_code, -9)
339-
self.assertEqual(err.work_dir, work_dir)
340-
self.assertEqual(err.output, '')
341-
self.assertEqual(err.stderr, None)
338+
self.assertEqual(err.cmd_result.exit_code, -9)
339+
self.assertEqual(err.cmd_result.work_dir, work_dir)
340+
self.assertEqual(err.cmd_result.output, '')
341+
self.assertEqual(err.cmd_result.stderr, None)
342342
self.assertTrue(isinstance(err.caller_info, tuple))
343343
self.assertEqual(len(err.caller_info), 3)
344344
self.assertEqual(err.caller_info[0], __file__)
@@ -356,7 +356,7 @@ def handler(signum, _):
356356
r"^\s+exit code\s* -> -9",
357357
r"^\s+working directory\s* -> " + work_dir,
358358
r"^\s+called from\s* -> 'test_run_shell_cmd_fail' function in .*/test/.*/run.py \(line [0-9]+\)",
359-
r"^\s+output \(stdout \+ stderr\)\s* -> .*/shell-cmd-error-.*/kill.out",
359+
r"^\s+output \(stdout \+ stderr\)\s* -> .*/run-shell-cmd-.*/kill.out",
360360
]
361361
for pattern in patterns:
362362
regex = re.compile(pattern, re.M)
@@ -368,12 +368,12 @@ def handler(signum, _):
368368
self.assertFalse("This should never be reached, RunShellCmdError should occur!")
369369
except RunShellCmdError as err:
370370
self.assertEqual(str(err), "Shell command 'kill' failed!")
371-
self.assertEqual(err.cmd, "kill -9 $$")
371+
self.assertEqual(err.cmd_result.cmd, "kill -9 $$")
372372
self.assertEqual(err.cmd_name, 'kill')
373-
self.assertEqual(err.exit_code, -9)
374-
self.assertEqual(err.work_dir, work_dir)
375-
self.assertEqual(err.output, '')
376-
self.assertEqual(err.stderr, '')
373+
self.assertEqual(err.cmd_result.exit_code, -9)
374+
self.assertEqual(err.cmd_result.work_dir, work_dir)
375+
self.assertEqual(err.cmd_result.output, '')
376+
self.assertEqual(err.cmd_result.stderr, '')
377377
self.assertTrue(isinstance(err.caller_info, tuple))
378378
self.assertEqual(len(err.caller_info), 3)
379379
self.assertEqual(err.caller_info[0], __file__)
@@ -391,8 +391,8 @@ def handler(signum, _):
391391
r"^\s+exit code\s+ -> -9",
392392
r"^\s+working directory\s+ -> " + work_dir,
393393
r"^\s+called from\s+ -> 'test_run_shell_cmd_fail' function in .*/test/.*/run.py \(line [0-9]+\)",
394-
r"^\s+output \(stdout\)\s+ -> .*/shell-cmd-error-.*/kill.out",
395-
r"^\s+error/warnings \(stderr\)\s+ -> .*/shell-cmd-error-.*/kill.err",
394+
r"^\s+output \(stdout\)\s+ -> .*/run-shell-cmd-.*/kill.out",
395+
r"^\s+error/warnings \(stderr\)\s+ -> .*/run-shell-cmd-.*/kill.err",
396396
]
397397
for pattern in patterns:
398398
regex = re.compile(pattern, re.M)
@@ -618,7 +618,7 @@ def test_run_shell_cmd_trace(self):
618618
r"^ >> running command:",
619619
r"\t\[started at: .*\]",
620620
r"\t\[working dir: .*\]",
621-
r"\t\[output logged in .*\]",
621+
r"\t\[output saved to .*\]",
622622
r"\techo hello",
623623
r" >> command completed: exit 0, ran in .*",
624624
]
@@ -678,7 +678,7 @@ def test_run_shell_cmd_trace_stdin(self):
678678
r"^ >> running command:",
679679
r"\t\[started at: [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]\]",
680680
r"\t\[working dir: .*\]",
681-
r"\t\[output logged in .*\]",
681+
r"\t\[output saved to .*\]",
682682
r"\techo hello",
683683
r" >> command completed: exit 0, ran in .*",
684684
]
@@ -909,7 +909,7 @@ def test_run_shell_cmd_cache(self):
909909
# inject value into cache to check whether executing command again really returns cached value
910910
with self.mocked_stdout_stderr():
911911
cached_res = RunShellCmdResult(cmd=cmd, output="123456", exit_code=123, stderr=None,
912-
work_dir='/test_ulimit')
912+
work_dir='/test_ulimit', out_file='/tmp/foo.out', err_file=None)
913913
run_shell_cmd.update_cache({(cmd, None): cached_res})
914914
res = run_shell_cmd(cmd)
915915
self.assertEqual(res.cmd, cmd)
@@ -927,7 +927,8 @@ def test_run_shell_cmd_cache(self):
927927

928928
# inject different output for cat with 'foo' as stdin to check whether cached value is used
929929
with self.mocked_stdout_stderr():
930-
cached_res = RunShellCmdResult(cmd=cmd, output="bar", exit_code=123, stderr=None, work_dir='/test_cat')
930+
cached_res = RunShellCmdResult(cmd=cmd, output="bar", exit_code=123, stderr=None,
931+
work_dir='/test_cat', out_file='/tmp/cat.out', err_file=None)
931932
run_shell_cmd.update_cache({(cmd, 'foo'): cached_res})
932933
res = run_shell_cmd(cmd, stdin='foo')
933934
self.assertEqual(res.cmd, cmd)

0 commit comments

Comments
 (0)