1212import os
1313import subprocess
1414import sys
15+ import time
1516
1617from .global_constants import LOCAL_PATH_INDICATOR , LOG_FILE_NAME
1718
@@ -198,6 +199,82 @@ def expand_local_url(url, field):
198199# subprocess
199200#
200201# ---------------------------------------------------------------------
202+ _TIMEOUT_MSG = """ Timout errors typically occur when svn or git requires
203+ authentication to access a private repository. On some systems, svn
204+ and git requests for authentication information will not be displayed
205+ to the user. In this case, the program will appear to hang and
206+ generate a timeout error. Ensure you can run svn and git manually and
207+ access all repositories without entering your authentication
208+ information."""
209+
210+ _TIMEOUT_SEC = 300
211+ _POLL_DELTA_SEC = 0.02
212+
213+
214+ def _poll_subprocess (commands , status_to_caller , output_to_caller ,
215+ timeout_sec = _TIMEOUT_SEC ):
216+ """Create a subprocess and poll the process until complete.
217+
218+ Impose a timeout limit and checkout process output for known
219+ conditions that require user interaction.
220+
221+ NOTE: the timeout_delta has significant impact on run time. If it
222+ is too long, and the many quick local subprocess calls will
223+ drastically increase the run time, especially in tests.
224+
225+ NOTE: This function is broken out into for ease of
226+ understanding. It does no error checking. It should only be called
227+ from execute_subprocess, never directly.
228+
229+ """
230+ logging .info (' ' .join (commands ))
231+ output = []
232+ start = time .time ()
233+
234+ proc = subprocess .Popen (commands ,
235+ shell = False ,
236+ stdout = subprocess .PIPE ,
237+ stderr = subprocess .STDOUT ,
238+ universal_newlines = True )
239+ while proc .poll () is None :
240+ time .sleep (_POLL_DELTA_SEC )
241+ if time .time () - start > timeout_sec :
242+ proc .kill ()
243+ time .sleep (_POLL_DELTA_SEC * 5 )
244+ msg = ("subprocess call to '{0}' has exceeded timeout limit of "
245+ "{1} seconds.\n {2}" .format (commands [0 ], timeout_sec ,
246+ _TIMEOUT_MSG ))
247+ fatal_error (msg )
248+ finish = time .time ()
249+
250+ run_time_msg = "run time : {0:.2f} seconds" .format (finish - start )
251+ logging .info (run_time_msg )
252+ output = proc .stdout .read ()
253+ log_process_output (output )
254+ status = proc .returncode
255+
256+ # NOTE(bja, 2018-03) need to cleanup open files. In python3 use
257+ # "with subprocess.Popen(...) as proc:", but that is not available
258+ # with python2 unless we create a context manager.
259+ proc .stdout .close ()
260+
261+ if status != 0 :
262+ raise subprocess .CalledProcessError (returncode = status ,
263+ cmd = commands ,
264+ output = output )
265+
266+ if status_to_caller and output_to_caller :
267+ ret_value = (status , output )
268+ elif status_to_caller :
269+ ret_value = status
270+ elif output_to_caller :
271+ ret_value = output
272+ else :
273+ ret_value = None
274+
275+ return ret_value
276+
277+
201278def execute_subprocess (commands , status_to_caller = False ,
202279 output_to_caller = False ):
203280 """Wrapper around subprocess.check_output to handle common
@@ -211,20 +288,30 @@ def execute_subprocess(commands, status_to_caller=False,
211288 return code, otherwise execute_subprocess treats non-zero return
212289 status as an error and raises an exception.
213290
291+ NOTE(bja, 2018-03) if the user doesn't have credentials setup
292+ correctly, then svn and git will prompt for a username/password or
293+ accepting the domain as trusted. We need to detect this and print
294+ enough info for the user to determine what happened and enter the
295+ appropriate information. When we detect some pre-determined
296+ conditions, we turn on screen output so the user can see what is
297+ needed. There doesn't appear to be a way to detect if the user
298+ entered any information in the terminal. So there is no way to
299+ turn off output.
300+
301+ NOTE(bja, 2018-03) we are polling the running process to avoid
302+ having it hang indefinitely if there is input that we don't
303+ detect. Some large checkouts are multiple minutes long. For now we
304+ are setting the timeout interval to five minutes.
305+
214306 """
215307 msg = 'In directory: {0}\n execute_subprocess running command:' .format (
216308 os .getcwd ())
217309 logging .info (msg )
218310 logging .info (commands )
219311 return_to_caller = status_to_caller or output_to_caller
220- status = - 1
221- output = ''
222312 try :
223- logging .info (' ' .join (commands ))
224- output = subprocess .check_output (commands , stderr = subprocess .STDOUT ,
225- universal_newlines = True )
226- log_process_output (output )
227- status = 0
313+ ret_value = _poll_subprocess (
314+ commands , status_to_caller , output_to_caller )
228315 except OSError as error :
229316 msg = failed_command_msg (
230317 'Command execution failed. Does the executable exist?' ,
@@ -246,24 +333,12 @@ def execute_subprocess(commands, status_to_caller=False,
246333 if not return_to_caller :
247334 msg_context = ('Process did not run successfully; '
248335 'returned status {0}' .format (error .returncode ))
249- msg = failed_command_msg (
250- msg_context ,
251- commands ,
252- output = error .output )
336+ msg = failed_command_msg (msg_context , commands ,
337+ output = error .output )
253338 logging .error (error )
254- logging .error (msg )
255339 log_process_output (error .output )
256340 fatal_error (msg )
257- status = error .returncode
258-
259- if status_to_caller and output_to_caller :
260- ret_value = (status , output )
261- elif status_to_caller :
262- ret_value = status
263- elif output_to_caller :
264- ret_value = output
265- else :
266- ret_value = None
341+ ret_value = error .returncode
267342
268343 return ret_value
269344
0 commit comments