Skip to content

Commit 6b09872

Browse files
authored
Merge pull request #4356 from boegel/run_shell_cmd_out_files
change `run_shell_cmd` to store command output in temporary file(s) by default + pass `RunShellCmdResult` instance to `RunShellCmdError`
2 parents c073716 + 8620e64 commit 6b09872

File tree

5 files changed

+97
-70
lines changed

5 files changed

+97
-70
lines changed

easybuild/tools/modules.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ def check_module_function(self, allow_mismatch=False, regex=None):
312312
output, exit_code = None, 1
313313
else:
314314
cmd = "type module"
315-
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=False, hidden=True)
315+
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=False, hidden=True, output_file=False)
316316
output, exit_code = res.output, res.exit_code
317317

318318
if regex is None:
@@ -821,7 +821,7 @@ def run_module(self, *args, **kwargs):
821821
cmd = ' '.join(cmd_list)
822822
# note: module commands are always run in dry mode, and are kept hidden in trace and dry run output
823823
res = run_shell_cmd(cmd_list, env=environ, fail_on_error=False, shell=False, split_stderr=True,
824-
hidden=True, in_dry_run=True)
824+
hidden=True, in_dry_run=True, output_file=False)
825825

826826
# stdout will contain python code (to change environment etc)
827827
# stderr will contain text (just like the normal module command)

easybuild/tools/run.py

Lines changed: 61 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -76,19 +76,23 @@
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 = cmd_result.cmd
88+
self.cmd_name = os.path.basename(self.cmd.split(' ')[0])
89+
self.exit_code = cmd_result.exit_code
90+
self.work_dir = cmd_result.work_dir
91+
self.output = cmd_result.output
92+
self.out_file = cmd_result.out_file
93+
self.stderr = cmd_result.stderr
94+
self.err_file = cmd_result.err_file
95+
9296
self.caller_info = caller_info
9397

9498
msg = f"Shell command '{self.cmd_name}' failed!"
@@ -110,21 +114,13 @@ def pad_4_spaces(msg):
110114
pad_4_spaces(f"working directory -> {self.work_dir}"),
111115
]
112116

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+
if self.out_file is not None:
118+
# if there's no separate file for error/warnings, then out_file includes both stdout + stderr
119+
out_info_msg = "output (stdout + stderr)" if self.err_file is None else "output (stdout) "
120+
error_info.append(pad_4_spaces(f"{out_info_msg} -> {self.out_file}"))
117121

118-
if self.stderr is None:
119-
error_info.append(pad_4_spaces(f"output (stdout + stderr) -> {output_fp}"))
120-
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)
124-
error_info.extend([
125-
pad_4_spaces(f"output (stdout) -> {output_fp}"),
126-
pad_4_spaces(f"error/warnings (stderr) -> {stderr_fp}"),
127-
])
122+
if self.err_file is not None:
123+
error_info.append(pad_4_spaces(f"error/warnings (stderr) -> {self.err_file}"))
128124

129125
caller_file_name, caller_line_nr, caller_function_name = self.caller_info
130126
called_from_info = f"'{caller_function_name}' function in {caller_file_name} (line {caller_line_nr})"
@@ -136,9 +132,9 @@ def pad_4_spaces(msg):
136132
sys.stderr.write('\n'.join(error_info) + '\n')
137133

138134

139-
def raise_run_shell_cmd_error(cmd, exit_code, work_dir, output, stderr):
135+
def raise_run_shell_cmd_error(cmd_res):
140136
"""
141-
Raise RunShellCmdError for failing shell command, after collecting additional caller info
137+
Raise RunShellCmdError for failed shell command, after collecting additional caller info
142138
"""
143139

144140
# figure out where failing command was run
@@ -150,7 +146,7 @@ def raise_run_shell_cmd_error(cmd, exit_code, work_dir, output, stderr):
150146
frameinfo = inspect.getouterframes(inspect.currentframe())[3]
151147
caller_info = (frameinfo.filename, frameinfo.lineno, frameinfo.function)
152148

153-
raise RunShellCmdError(cmd, exit_code, work_dir, output, stderr, caller_info)
149+
raise RunShellCmdError(cmd_res, caller_info)
154150

155151

156152
def run_cmd_cache(func):
@@ -185,7 +181,7 @@ def cache_aware_func(cmd, *args, **kwargs):
185181
@run_shell_cmd_cache
186182
def run_shell_cmd(cmd, fail_on_error=True, split_stderr=False, stdin=None, env=None,
187183
hidden=False, in_dry_run=False, verbose_dry_run=False, work_dir=None, shell=True,
188-
output_file=False, stream_output=False, asynchronous=False, with_hooks=True,
184+
output_file=True, stream_output=False, asynchronous=False, with_hooks=True,
189185
qa_patterns=None, qa_wait_patterns=None):
190186
"""
191187
Run specified (interactive) shell command, and capture output + exit code.
@@ -234,16 +230,23 @@ def to_cmd_str(cmd):
234230
if work_dir is None:
235231
work_dir = os.getcwd()
236232

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-
246233
cmd_str = to_cmd_str(cmd)
234+
cmd_name = os.path.basename(cmd_str.split(' ')[0])
235+
236+
# temporary output file(s) for command output
237+
if output_file:
238+
toptmpdir = os.path.join(tempfile.gettempdir(), 'run-shell-cmd-output')
239+
os.makedirs(toptmpdir, exist_ok=True)
240+
tmpdir = tempfile.mkdtemp(dir=toptmpdir, prefix=f'{cmd_name}-')
241+
cmd_out_fp = os.path.join(tmpdir, 'out.txt')
242+
_log.info(f'run_cmd: Output of "{cmd_str}" will be logged to {cmd_out_fp}')
243+
if split_stderr:
244+
cmd_err_fp = os.path.join(tmpdir, 'err.txt')
245+
_log.info(f'run_cmd: Errors and warnings of "{cmd_str}" will be logged to {cmd_err_fp}')
246+
else:
247+
cmd_err_fp = None
248+
else:
249+
cmd_out_fp, cmd_err_fp = None, None
247250

248251
# early exit in 'dry run' mode, after printing the command that would be run (unless 'hidden' is enabled)
249252
if not in_dry_run and build_option('extended_dry_run'):
@@ -253,11 +256,12 @@ def to_cmd_str(cmd):
253256
msg += f" (in {work_dir})"
254257
dry_run_msg(msg, silent=silent)
255258

256-
return RunShellCmdResult(cmd=cmd_str, exit_code=0, output='', stderr=None, work_dir=work_dir)
259+
return RunShellCmdResult(cmd=cmd_str, exit_code=0, output='', stderr=None, work_dir=work_dir,
260+
out_file=cmd_out_fp, err_file=cmd_err_fp)
257261

258262
start_time = datetime.now()
259263
if not hidden:
260-
cmd_trace_msg(cmd_str, start_time, work_dir, stdin, cmd_out_fp)
264+
cmd_trace_msg(cmd_str, start_time, work_dir, stdin, cmd_out_fp, cmd_err_fp)
261265

262266
if stdin:
263267
# 'input' value fed to subprocess.run must be a byte sequence
@@ -286,7 +290,19 @@ def to_cmd_str(cmd):
286290
output = proc.stdout.decode('utf-8', 'ignore')
287291
stderr = proc.stderr.decode('utf-8', 'ignore') if split_stderr else None
288292

289-
res = RunShellCmdResult(cmd=cmd_str, exit_code=proc.returncode, output=output, stderr=stderr, work_dir=work_dir)
293+
# store command output to temporary file(s)
294+
if output_file:
295+
try:
296+
with open(cmd_out_fp, 'w') as fp:
297+
fp.write(output)
298+
if split_stderr:
299+
with open(cmd_err_fp, 'w') as fp:
300+
fp.write(stderr)
301+
except IOError as err:
302+
raise EasyBuildError(f"Failed to dump command output to temporary file: {err}")
303+
304+
res = RunShellCmdResult(cmd=cmd_str, exit_code=proc.returncode, output=output, stderr=stderr, work_dir=work_dir,
305+
out_file=cmd_out_fp, err_file=cmd_err_fp)
290306

291307
# always log command output
292308
cmd_name = cmd_str.split(' ')[0]
@@ -301,7 +317,7 @@ def to_cmd_str(cmd):
301317
else:
302318
_log.warning(f"Shell command FAILED (exit code {res.exit_code}, see output above): {cmd_str}")
303319
if fail_on_error:
304-
raise_run_shell_cmd_error(res.cmd, res.exit_code, res.work_dir, output=res.output, stderr=res.stderr)
320+
raise_run_shell_cmd_error(res)
305321

306322
if with_hooks:
307323
run_hook_kwargs = {
@@ -319,15 +335,16 @@ def to_cmd_str(cmd):
319335
return res
320336

321337

322-
def cmd_trace_msg(cmd, start_time, work_dir, stdin, cmd_out_fp):
338+
def cmd_trace_msg(cmd, start_time, work_dir, stdin, cmd_out_fp, cmd_err_fp):
323339
"""
324340
Helper function to construct and print trace message for command being run
325341
326342
:param cmd: command being run
327343
:param start_time: datetime object indicating when command was started
328344
:param work_dir: path of working directory in which command is run
329345
:param stdin: stdin input value for command
330-
:param cmd_out_fp: path to output log file for command
346+
:param cmd_out_fp: path to output file for command
347+
:param cmd_err_fp: path to errors/warnings output file for command
331348
"""
332349
start_time = start_time.strftime('%Y-%m-%d %H:%M:%S')
333350

@@ -339,7 +356,9 @@ def cmd_trace_msg(cmd, start_time, work_dir, stdin, cmd_out_fp):
339356
if stdin:
340357
lines.append(f"\t[input: {stdin}]")
341358
if cmd_out_fp:
342-
lines.append(f"\t[output logged in {cmd_out_fp}]")
359+
lines.append(f"\t[output saved to {cmd_out_fp}]")
360+
if cmd_err_fp:
361+
lines.append(f"\t[errors/warnings saved to {cmd_err_fp}]")
343362

344363
lines.append('\t' + cmd)
345364

easybuild/tools/systemtools.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ def get_avail_core_count():
273273
core_cnt = int(sum(sched_getaffinity()))
274274
else:
275275
# BSD-type systems
276-
res = run_shell_cmd('sysctl -n hw.ncpu', in_dry_run=True, hidden=True, with_hooks=False)
276+
res = run_shell_cmd('sysctl -n hw.ncpu', in_dry_run=True, hidden=True, with_hooks=False, output_file=False)
277277
try:
278278
if int(res.output) > 0:
279279
core_cnt = int(res.output)
@@ -310,7 +310,7 @@ def get_total_memory():
310310
elif os_type == DARWIN:
311311
cmd = "sysctl -n hw.memsize"
312312
_log.debug("Trying to determine total memory size on Darwin via cmd '%s'", cmd)
313-
res = run_shell_cmd(cmd, in_dry_run=True, hidden=True, with_hooks=False)
313+
res = run_shell_cmd(cmd, in_dry_run=True, hidden=True, with_hooks=False, output_file=False)
314314
if res.exit_code == 0:
315315
memtotal = int(res.output.strip()) // (1024**2)
316316

@@ -392,14 +392,15 @@ def get_cpu_vendor():
392392

393393
elif os_type == DARWIN:
394394
cmd = "sysctl -n machdep.cpu.vendor"
395-
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False)
395+
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False, output_file=False)
396396
out = res.output.strip()
397397
if res.exit_code == 0 and out in VENDOR_IDS:
398398
vendor = VENDOR_IDS[out]
399399
_log.debug("Determined CPU vendor on DARWIN as being '%s' via cmd '%s" % (vendor, cmd))
400400
else:
401401
cmd = "sysctl -n machdep.cpu.brand_string"
402-
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False)
402+
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False,
403+
output_file=False)
403404
out = res.output.strip().split(' ')[0]
404405
if res.exit_code == 0 and out in CPU_VENDORS:
405406
vendor = out
@@ -502,7 +503,7 @@ def get_cpu_model():
502503

503504
elif os_type == DARWIN:
504505
cmd = "sysctl -n machdep.cpu.brand_string"
505-
res = run_shell_cmd(cmd, in_dry_run=True, hidden=True, with_hooks=False)
506+
res = run_shell_cmd(cmd, in_dry_run=True, hidden=True, with_hooks=False, output_file=False)
506507
if res.exit_code == 0:
507508
model = res.output.strip()
508509
_log.debug("Determined CPU model on Darwin using cmd '%s': %s" % (cmd, model))
@@ -547,7 +548,7 @@ def get_cpu_speed():
547548
elif os_type == DARWIN:
548549
cmd = "sysctl -n hw.cpufrequency_max"
549550
_log.debug("Trying to determine CPU frequency on Darwin via cmd '%s'" % cmd)
550-
res = run_shell_cmd(cmd, in_dry_run=True, hidden=True, with_hooks=False)
551+
res = run_shell_cmd(cmd, in_dry_run=True, hidden=True, with_hooks=False, output_file=False)
551552
out = res.output.strip()
552553
cpu_freq = None
553554
if res.exit_code == 0 and out:
@@ -595,7 +596,8 @@ def get_cpu_features():
595596
for feature_set in ['extfeatures', 'features', 'leaf7_features']:
596597
cmd = "sysctl -n machdep.cpu.%s" % feature_set
597598
_log.debug("Trying to determine CPU features on Darwin via cmd '%s'", cmd)
598-
res = run_shell_cmd(cmd, in_dry_run=True, hidden=True, fail_on_error=False, with_hooks=False)
599+
res = run_shell_cmd(cmd, in_dry_run=True, hidden=True, fail_on_error=False, with_hooks=False,
600+
output_file=False)
599601
if res.exit_code == 0:
600602
cpu_feat.extend(res.output.strip().lower().split())
601603

@@ -622,7 +624,8 @@ def get_gpu_info():
622624
try:
623625
cmd = "nvidia-smi --query-gpu=gpu_name,driver_version --format=csv,noheader"
624626
_log.debug("Trying to determine NVIDIA GPU info on Linux via cmd '%s'", cmd)
625-
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False)
627+
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False,
628+
output_file=False)
626629
if res.exit_code == 0:
627630
for line in res.output.strip().split('\n'):
628631
nvidia_gpu_info = gpu_info.setdefault('NVIDIA', {})
@@ -640,13 +643,15 @@ def get_gpu_info():
640643
try:
641644
cmd = "rocm-smi --showdriverversion --csv"
642645
_log.debug("Trying to determine AMD GPU driver on Linux via cmd '%s'", cmd)
643-
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False)
646+
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False,
647+
output_file=False)
644648
if res.exit_code == 0:
645649
amd_driver = res.output.strip().split('\n')[1].split(',')[1]
646650

647651
cmd = "rocm-smi --showproductname --csv"
648652
_log.debug("Trying to determine AMD GPU info on Linux via cmd '%s'", cmd)
649-
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False)
653+
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, with_hooks=False,
654+
output_file=False)
650655
if res.exit_code == 0:
651656
for line in res.output.strip().split('\n')[1:]:
652657
amd_card_series = line.split(',')[1]
@@ -865,7 +870,7 @@ def check_os_dependency(dep):
865870
pkg_cmd_flag.get(pkg_cmd),
866871
dep,
867872
])
868-
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True)
873+
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, output_file=False)
869874
found = res.exit_code == 0
870875
if found:
871876
break
@@ -877,7 +882,7 @@ def check_os_dependency(dep):
877882
# try locate if it's available
878883
if not found and which('locate'):
879884
cmd = 'locate -c --regexp "/%s$"' % dep
880-
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True)
885+
res = run_shell_cmd(cmd, fail_on_error=False, in_dry_run=True, hidden=True, output_file=False)
881886
try:
882887
found = (res.exit_code == 0 and int(res.output.strip()) > 0)
883888
except ValueError:
@@ -893,7 +898,7 @@ def get_tool_version(tool, version_option='--version', ignore_ec=False):
893898
Output is returned as a single-line string (newlines are replaced by '; ').
894899
"""
895900
res = run_shell_cmd(' '.join([tool, version_option]), fail_on_error=False, in_dry_run=True,
896-
hidden=True, with_hooks=False)
901+
hidden=True, with_hooks=False, output_file=False)
897902
if not ignore_ec and res.exit_code:
898903
_log.warning("Failed to determine version of %s using '%s %s': %s" % (tool, tool, version_option, res.output))
899904
return UNKNOWN
@@ -905,7 +910,7 @@ def get_gcc_version():
905910
"""
906911
Process `gcc --version` and return the GCC version.
907912
"""
908-
res = run_shell_cmd('gcc --version', fail_on_error=False, in_dry_run=True, hidden=True)
913+
res = run_shell_cmd('gcc --version', fail_on_error=False, in_dry_run=True, hidden=True, output_file=False)
909914
gcc_ver = None
910915
if res.exit_code:
911916
_log.warning("Failed to determine the version of GCC: %s", res.output)
@@ -961,7 +966,7 @@ def get_linked_libs_raw(path):
961966
or None for other types of files.
962967
"""
963968

964-
res = run_shell_cmd("file %s" % path, fail_on_error=False, hidden=True)
969+
res = run_shell_cmd("file %s" % path, fail_on_error=False, hidden=True, output_file=False)
965970
if res.exit_code:
966971
fail_msg = "Failed to run 'file %s': %s" % (path, res.output)
967972
_log.warning(fail_msg)
@@ -996,7 +1001,7 @@ def get_linked_libs_raw(path):
9961001
# take into account that 'ldd' may fail for strange reasons,
9971002
# like printing 'not a dynamic executable' when not enough memory is available
9981003
# (see also https://bugzilla.redhat.com/show_bug.cgi?id=1817111)
999-
res = run_shell_cmd(linked_libs_cmd, fail_on_error=False, hidden=True)
1004+
res = run_shell_cmd(linked_libs_cmd, fail_on_error=False, hidden=True, output_file=False)
10001005
if res.exit_code == 0:
10011006
linked_libs_out = res.output
10021007
else:
@@ -1178,7 +1183,7 @@ def get_default_parallelism():
11781183
# No cache -> Calculate value from current system values
11791184
par = get_avail_core_count()
11801185
# determine max user processes via ulimit -u
1181-
res = run_shell_cmd("ulimit -u", in_dry_run=True, hidden=True)
1186+
res = run_shell_cmd("ulimit -u", in_dry_run=True, hidden=True, output_file=False)
11821187
try:
11831188
if res.output.startswith("unlimited"):
11841189
maxuserproc = 2 ** 32 - 1

0 commit comments

Comments
 (0)