4949from collections import namedtuple
5050from datetime import datetime
5151
52+ try :
53+ # get_native_id is only available in Python >= 3.8
54+ from threading import get_native_id as get_thread_id
55+ except ImportError :
56+ # get_ident is available in Python >= 3.3
57+ from threading import get_ident as get_thread_id
58+
5259import easybuild .tools .asyncprocess as asyncprocess
5360from easybuild .base import fancylogger
5461from easybuild .tools .build_log import EasyBuildError , dry_run_msg , print_msg , time_str_since
7986
8087
8188RunShellCmdResult = namedtuple ('RunShellCmdResult' , ('cmd' , 'exit_code' , 'output' , 'stderr' , 'work_dir' ,
82- 'out_file' , 'err_file' ))
89+ 'out_file' , 'err_file' , 'thread_id' , 'task_id' ))
8390
8491
8592class RunShellCmdError (BaseException ):
@@ -183,7 +190,7 @@ def cache_aware_func(cmd, *args, **kwargs):
183190@run_shell_cmd_cache
184191def run_shell_cmd (cmd , fail_on_error = True , split_stderr = False , stdin = None , env = None ,
185192 hidden = False , in_dry_run = False , verbose_dry_run = False , work_dir = None , use_bash = True ,
186- output_file = True , stream_output = None , asynchronous = False , with_hooks = True ,
193+ output_file = True , stream_output = None , asynchronous = False , task_id = None , with_hooks = True ,
187194 qa_patterns = None , qa_wait_patterns = None ):
188195 """
189196 Run specified (interactive) shell command, and capture output + exit code.
@@ -199,7 +206,8 @@ def run_shell_cmd(cmd, fail_on_error=True, split_stderr=False, stdin=None, env=N
199206 :param use_bash: execute command through bash shell (enabled by default)
200207 :param output_file: collect command output in temporary output file
201208 :param stream_output: stream command output to stdout (auto-enabled with --logtostdout if None)
202- :param asynchronous: run command asynchronously
209+ :param asynchronous: indicate that command is being run asynchronously
210+ :param task_id: task ID for specified shell command (included in return value)
203211 :param with_hooks: trigger pre/post run_shell_cmd hooks (if defined)
204212 :param qa_patterns: list of 2-tuples with patterns for questions + corresponding answers
205213 :param qa_wait_patterns: list of 2-tuples with patterns for non-questions
@@ -223,9 +231,6 @@ def to_cmd_str(cmd):
223231 return cmd_str
224232
225233 # temporarily raise a NotImplementedError until all options are implemented
226- if asynchronous :
227- raise NotImplementedError
228-
229234 if qa_patterns or qa_wait_patterns :
230235 raise NotImplementedError
231236
@@ -235,6 +240,11 @@ def to_cmd_str(cmd):
235240 cmd_str = to_cmd_str (cmd )
236241 cmd_name = os .path .basename (cmd_str .split (' ' )[0 ])
237242
243+ thread_id = None
244+ if asynchronous :
245+ thread_id = get_thread_id ()
246+ _log .info (f"Initiating running of shell command '{ cmd_str } ' via thread with ID { thread_id } " )
247+
238248 # auto-enable streaming of command output under --logtostdout/-l, unless it was disabled explicitely
239249 if stream_output is None and build_option ('logtostdout' ):
240250 _log .info (f"Auto-enabling streaming output of '{ cmd_str } ' command because logging to stdout is enabled" )
@@ -259,16 +269,16 @@ def to_cmd_str(cmd):
259269 if not in_dry_run and build_option ('extended_dry_run' ):
260270 if not hidden or verbose_dry_run :
261271 silent = build_option ('silent' )
262- msg = f" running command \" { cmd_str } \" \n "
272+ msg = f" running shell command \" { cmd_str } \" \n "
263273 msg += f" (in { work_dir } )"
264274 dry_run_msg (msg , silent = silent )
265275
266276 return RunShellCmdResult (cmd = cmd_str , exit_code = 0 , output = '' , stderr = None , work_dir = work_dir ,
267- out_file = cmd_out_fp , err_file = cmd_err_fp )
277+ out_file = cmd_out_fp , err_file = cmd_err_fp , thread_id = thread_id , task_id = task_id )
268278
269279 start_time = datetime .now ()
270280 if not hidden :
271- cmd_trace_msg (cmd_str , start_time , work_dir , stdin , cmd_out_fp , cmd_err_fp )
281+ _cmd_trace_msg (cmd_str , start_time , work_dir , stdin , cmd_out_fp , cmd_err_fp , thread_id )
272282
273283 if stream_output :
274284 print_msg (f"(streaming) output for command '{ cmd_str } ':" )
@@ -293,7 +303,11 @@ def to_cmd_str(cmd):
293303
294304 stderr = subprocess .PIPE if split_stderr else subprocess .STDOUT
295305
296- _log .info (f"Running command '{ cmd_str } ' in { work_dir } " )
306+ log_msg = f"Running shell command '{ cmd_str } ' in { work_dir } "
307+ if thread_id :
308+ log_msg += f" (via thread with ID { thread_id } )"
309+ _log .info (log_msg )
310+
297311 proc = subprocess .Popen (cmd , stdout = subprocess .PIPE , stderr = stderr , stdin = subprocess .PIPE ,
298312 cwd = work_dir , env = env , shell = shell , executable = executable )
299313
@@ -337,7 +351,7 @@ def to_cmd_str(cmd):
337351 raise EasyBuildError (f"Failed to dump command output to temporary file: { err } " )
338352
339353 res = RunShellCmdResult (cmd = cmd_str , exit_code = proc .returncode , output = output , stderr = stderr , work_dir = work_dir ,
340- out_file = cmd_out_fp , err_file = cmd_err_fp )
354+ out_file = cmd_out_fp , err_file = cmd_err_fp , thread_id = thread_id , task_id = task_id )
341355
342356 # always log command output
343357 cmd_name = cmd_str .split (' ' )[0 ]
@@ -370,7 +384,7 @@ def to_cmd_str(cmd):
370384 return res
371385
372386
373- def cmd_trace_msg (cmd , start_time , work_dir , stdin , cmd_out_fp , cmd_err_fp ):
387+ def _cmd_trace_msg (cmd , start_time , work_dir , stdin , cmd_out_fp , cmd_err_fp , thread_id ):
374388 """
375389 Helper function to construct and print trace message for command being run
376390
@@ -380,11 +394,18 @@ def cmd_trace_msg(cmd, start_time, work_dir, stdin, cmd_out_fp, cmd_err_fp):
380394 :param stdin: stdin input value for command
381395 :param cmd_out_fp: path to output file for command
382396 :param cmd_err_fp: path to errors/warnings output file for command
397+ :param thread_id: thread ID (None when not running shell command asynchronously)
383398 """
384399 start_time = start_time .strftime ('%Y-%m-%d %H:%M:%S' )
385400
401+ if thread_id :
402+ run_cmd_msg = f"running shell command (asynchronously, thread ID: { thread_id } ):"
403+ else :
404+ run_cmd_msg = "running shell command:"
405+
386406 lines = [
387- "running command:" ,
407+ run_cmd_msg ,
408+ f"\t { cmd } " ,
388409 f"\t [started at: { start_time } ]" ,
389410 f"\t [working dir: { work_dir } ]" ,
390411 ]
@@ -395,8 +416,6 @@ def cmd_trace_msg(cmd, start_time, work_dir, stdin, cmd_out_fp, cmd_err_fp):
395416 if cmd_err_fp :
396417 lines .append (f"\t [errors/warnings saved to { cmd_err_fp } ]" )
397418
398- lines .append ('\t ' + cmd )
399-
400419 trace_msg ('\n ' .join (lines ))
401420
402421
0 commit comments