3737"""
3838import contextlib
3939import functools
40+ import inspect
4041import os
4142import re
4243import signal
7576]
7677
7778
78- RunResult = namedtuple ('RunResult' , ('cmd' , 'exit_code' , 'output' , 'stderr' , 'work_dir' ))
79+ RunShellCmdResult = namedtuple ('RunShellCmdResult' , ('cmd' , 'exit_code' , 'output' , 'stderr' , 'work_dir' ))
80+
81+
82+ class RunShellCmdError (BaseException ):
83+
84+ def __init__ (self , cmd , exit_code , work_dir , output , stderr , caller_info , * args , ** kwargs ):
85+ """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
92+ self .caller_info = caller_info
93+
94+ msg = f"Shell command '{ self .cmd_name } ' failed!"
95+ super (RunShellCmdError , self ).__init__ (msg , * args , ** kwargs )
96+
97+ def print (self ):
98+ """
99+ Report failed shell command for this RunShellCmdError instance
100+ """
101+
102+ def pad_4_spaces (msg ):
103+ return ' ' * 4 + msg
104+
105+ error_info = [
106+ '' ,
107+ "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 } " ),
111+ ]
112+
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 } " ))
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+ ])
128+
129+ caller_file_name , caller_line_nr , caller_function_name = self .caller_info
130+ called_from_info = f"'{ caller_function_name } ' function in { caller_file_name } (line { caller_line_nr } )"
131+ error_info .extend ([
132+ pad_4_spaces (f"called from -> { called_from_info } " ),
133+ '' ,
134+ ])
135+
136+ sys .stderr .write ('\n ' .join (error_info ) + '\n ' )
137+
138+
139+ def raise_run_shell_cmd_error (cmd , exit_code , work_dir , output , stderr ):
140+ """
141+ Raise RunShellCmdError for failing shell command, after collecting additional caller info
142+ """
143+
144+ # figure out where failing command was run
145+ # need to go 3 levels down:
146+ # 1) this function
147+ # 2) run_shell_cmd function
148+ # 3) run_cmd_cache decorator
149+ # 4) actual caller site
150+ frameinfo = inspect .getouterframes (inspect .currentframe ())[3 ]
151+ caller_info = (frameinfo .filename , frameinfo .lineno , frameinfo .function )
152+
153+ raise RunShellCmdError (cmd , exit_code , work_dir , output , stderr , caller_info )
79154
80155
81156def run_cmd_cache (func ):
@@ -178,7 +253,7 @@ def to_cmd_str(cmd):
178253 msg += f" (in { work_dir } )"
179254 dry_run_msg (msg , silent = silent )
180255
181- return RunResult (cmd = cmd_str , exit_code = 0 , output = '' , stderr = None , work_dir = work_dir )
256+ return RunShellCmdResult (cmd = cmd_str , exit_code = 0 , output = '' , stderr = None , work_dir = work_dir )
182257
183258 start_time = datetime .now ()
184259 if not hidden :
@@ -204,14 +279,29 @@ def to_cmd_str(cmd):
204279 _log .info ("Command to run was changed by pre-%s hook: '%s' (was: '%s')" , RUN_SHELL_CMD , cmd , old_cmd )
205280
206281 _log .info (f"Running command '{ cmd_str } ' in { work_dir } " )
207- proc = subprocess .run (cmd , stdout = subprocess .PIPE , stderr = stderr , check = fail_on_error ,
282+ proc = subprocess .run (cmd , stdout = subprocess .PIPE , stderr = stderr , check = False ,
208283 cwd = work_dir , env = env , input = stdin , shell = shell , executable = executable )
209284
210285 # return output as a regular string rather than a byte sequence (and non-UTF-8 characters get stripped out)
211286 output = proc .stdout .decode ('utf-8' , 'ignore' )
212- stderr_output = proc .stderr .decode ('utf-8' , 'ignore' ) if split_stderr else None
287+ stderr = proc .stderr .decode ('utf-8' , 'ignore' ) if split_stderr else None
288+
289+ res = RunShellCmdResult (cmd = cmd_str , exit_code = proc .returncode , output = output , stderr = stderr , work_dir = work_dir )
213290
214- res = RunResult (cmd = cmd_str , exit_code = proc .returncode , output = output , stderr = stderr_output , work_dir = work_dir )
291+ # always log command output
292+ cmd_name = cmd_str .split (' ' )[0 ]
293+ if split_stderr :
294+ _log .info (f"Output of '{ cmd_name } ...' shell command (stdout only):\n { res .output } " )
295+ _log .info (f"Warnings and errors of '{ cmd_name } ...' shell command (stderr only):\n { res .stderr } " )
296+ else :
297+ _log .info (f"Output of '{ cmd_name } ...' shell command (stdout + stderr):\n { res .output } " )
298+
299+ if res .exit_code == 0 :
300+ _log .info (f"Shell command completed successfully (see output above): { cmd_str } " )
301+ else :
302+ _log .warning (f"Shell command FAILED (exit code { res .exit_code } , see output above): { cmd_str } " )
303+ if fail_on_error :
304+ raise_run_shell_cmd_error (res .cmd , res .exit_code , res .work_dir , output = res .output , stderr = res .stderr )
215305
216306 if with_hooks :
217307 run_hook_kwargs = {
@@ -222,13 +312,6 @@ def to_cmd_str(cmd):
222312 }
223313 run_hook (RUN_SHELL_CMD , hooks , post_step_hook = True , args = [cmd ], kwargs = run_hook_kwargs )
224314
225- if split_stderr :
226- log_msg = f"Command '{ cmd_str } ' exited with exit code { res .exit_code } , "
227- log_msg += f"with stdout:\n { res .output } \n stderr:\n { res .stderr } "
228- else :
229- log_msg = f"Command '{ cmd_str } ' exited with exit code { res .exit_code } and output:\n { res .output } "
230- _log .info (log_msg )
231-
232315 if not hidden :
233316 time_since_start = time_str_since (start_time )
234317 trace_msg (f"command completed: exit { res .exit_code } , ran in { time_since_start } " )
0 commit comments