@@ -109,7 +109,7 @@ def handle_process_output(
109109 stderr_handler : Union [None , Callable [[AnyStr ], None ], Callable [[List [AnyStr ]], None ]],
110110 finalizer : Union [None , Callable [[Union [Popen , "Git.AutoInterrupt" ]], None ]] = None ,
111111 decode_streams : bool = True ,
112- kill_after_timeout : Optional [float , int ] = None ,
112+ kill_after_timeout : Optional [Union [ float , int ] ] = None ,
113113) -> None :
114114 R"""Register for notifications to learn that process output is ready to read, and
115115 dispatch lines to the respective line handlers.
@@ -335,7 +335,7 @@ def __init__(
335335 self ,
336336 proc : subprocess .Popen | None ,
337337 args : Any ,
338- timeout : Optional [float , int ] = None ,
338+ timeout : Optional [Union [ float , int ] ] = None ,
339339 ) -> None :
340340 self .proc = proc
341341 self .args = args
@@ -370,10 +370,15 @@ def _terminate(self) -> None:
370370 # Try to kill it.
371371 try :
372372 proc .terminate ()
373- status = proc .wait (timeout = self .timeout ) # Ensure the process goes away .
373+ status = proc .wait (timeout = self .timeout ) # Gracefully wait for the process to terminate .
374374
375375 self .status = self ._status_code_if_terminate or status
376- except (OSError , AttributeError , subprocess .TimeoutExpired ) as ex :
376+ except subprocess .TimeoutExpired :
377+ _logger .warning ("Process did not complete successfully in %s seconds; will forcefully kill" , exc_info = True )
378+ proc .kill ()
379+ status = proc .wait () # Ensure the process goes away by blocking with `timeout=None`.
380+ self .status = self ._status_code_if_terminate or status
381+ except (OSError , AttributeError ) as ex :
377382 # On interpreter shutdown (notably on Windows), parts of the stdlib used by
378383 # subprocess can already be torn down (e.g. `subprocess._winapi` becomes None),
379384 # which can cause AttributeError during terminate(). In that case, we prefer
@@ -389,7 +394,7 @@ def __getattr__(self, attr: str) -> Any:
389394 return getattr (self .proc , attr )
390395
391396 # TODO: Bad choice to mimic `proc.wait()` but with different args.
392- def wait (self , stderr : Union [None , str , bytes ] = b"" ) -> int :
397+ def wait (self , stderr : Optional [ Union [str , bytes ]] = b"" , timeout : Optional [ Union [ int , float ]] = None ) -> int :
393398 """Wait for the process and return its status code.
394399
395400 :param stderr:
@@ -406,7 +411,8 @@ def wait(self, stderr: Union[None, str, bytes] = b"") -> int:
406411 stderr_b = force_bytes (data = stderr , encoding = "utf-8" )
407412 status : Union [int , None ]
408413 if self .proc is not None :
409- status = self .proc .wait (timeout = self .timeout )
414+ timeout = self .timeout if timeout is None else timeout
415+ status = self .proc .wait (timeout = timeout )
410416 p_stderr = self .proc .stderr
411417 else : # Assume the underlying proc was killed earlier or never existed.
412418 status = self .status
@@ -1112,7 +1118,7 @@ def execute(
11121118 as_process : bool = False ,
11131119 output_stream : Union [None , BinaryIO ] = None ,
11141120 stdout_as_string : bool = True ,
1115- kill_after_timeout : Optional [float , int ] = None ,
1121+ kill_after_timeout : Optional [Union [ float , int ] ] = None ,
11161122 with_stdout : bool = True ,
11171123 universal_newlines : bool = False ,
11181124 shell : Union [None , bool ] = None ,
@@ -1325,8 +1331,9 @@ def execute(
13251331 "error: process killed because it timed out. kill_after_timeout=%s seconds" ,
13261332 kill_after_timeout ,
13271333 )
1328- proc .terminate ()
1329- stdout_value , stderr_value = proc .communicate ()
1334+ with contextlib .suppress (subprocess .TimeoutExpired ):
1335+ proc .kill ()
1336+ stdout_value , stderr_value = proc .communicate (timeout = 0.1 )
13301337 if with_exceptions :
13311338 raise GitCommandError (redacted_command , proc .returncode , stderr_value , stdout_value ) from err
13321339 finally :
0 commit comments