@@ -110,7 +110,7 @@ def handle_process_output(
110110 stderr_handler : Union [None , Callable [[AnyStr ], None ], Callable [[List [AnyStr ]], None ]],
111111 finalizer : Union [None , Callable [[Union [Popen , "Git.AutoInterrupt" ]], None ]] = None ,
112112 decode_streams : bool = True ,
113- kill_after_timeout : Union [ None , float ] = None ,
113+ kill_after_timeout : float | int | None = None ,
114114) -> None :
115115 R"""Register for notifications to learn that process output is ready to read, and
116116 dispatch lines to the respective line handlers.
@@ -139,7 +139,7 @@ def handle_process_output(
139139 - decoding must happen later, such as for :class:`~git.diff.Diff`\s.
140140
141141 :param kill_after_timeout:
142- :class:`float` or ``None``, Default = ``None``
142+ :class:`int`, ` float`, or ``None`` (block indefinitely) , Default = ``None``.
143143
144144 To specify a timeout in seconds for the git command, after which the process
145145 should be killed.
@@ -326,16 +326,21 @@ class _AutoInterrupt:
326326 raise.
327327 """
328328
329- __slots__ = ("proc" , "args" , "status" )
329+ __slots__ = ("proc" , "args" , "status" , "timeout" )
330330
331331 # If this is non-zero it will override any status code during _terminate, used
332332 # to prevent race conditions in testing.
333333 _status_code_if_terminate : int = 0
334334
335- def __init__ (self , proc : Union [None , subprocess .Popen ], args : Any ) -> None :
335+ def __init__ (self ,
336+ proc : subprocess .Popen | None ,
337+ args : Any ,
338+ timeout : float | int | None = None ,
339+ ) -> None :
336340 self .proc = proc
337341 self .args = args
338342 self .status : Union [int , None ] = None
343+ self .timeout = timeout
339344
340345 def _terminate (self ) -> None :
341346 """Terminate the underlying process."""
@@ -365,7 +370,7 @@ def _terminate(self) -> None:
365370 # Try to kill it.
366371 try :
367372 proc .terminate ()
368- status = proc .wait () # Ensure the process goes away.
373+ status = proc .wait (timeout = self . timeout ) # Ensure the process goes away.
369374
370375 self .status = self ._status_code_if_terminate or status
371376 except (OSError , AttributeError ) as ex :
@@ -400,7 +405,7 @@ def wait(self, stderr: Union[None, str, bytes] = b"") -> int:
400405 stderr_b = force_bytes (data = stderr , encoding = "utf-8" )
401406 status : Union [int , None ]
402407 if self .proc is not None :
403- status = self .proc .wait ()
408+ status = self .proc .wait (timeout = self . timeout )
404409 p_stderr = self .proc .stderr
405410 else : # Assume the underlying proc was killed earlier or never existed.
406411 status = self .status
@@ -1303,66 +1308,14 @@ def execute(
13031308 if as_process :
13041309 return self .AutoInterrupt (proc , command )
13051310
1306- if sys .platform != "win32" and kill_after_timeout is not None :
1307- # Help mypy figure out this is not None even when used inside communicate().
1308- timeout = kill_after_timeout
1309-
1310- def kill_process (pid : int ) -> None :
1311- """Callback to kill a process.
1312-
1313- This callback implementation would be ineffective and unsafe on Windows.
1314- """
1315- p = Popen (["ps" , "--ppid" , str (pid )], stdout = PIPE )
1316- child_pids = []
1317- if p .stdout is not None :
1318- for line in p .stdout :
1319- if len (line .split ()) > 0 :
1320- local_pid = (line .split ())[0 ]
1321- if local_pid .isdigit ():
1322- child_pids .append (int (local_pid ))
1323- try :
1324- os .kill (pid , signal .SIGKILL )
1325- for child_pid in child_pids :
1326- try :
1327- os .kill (child_pid , signal .SIGKILL )
1328- except OSError :
1329- pass
1330- # Tell the main routine that the process was killed.
1331- kill_check .set ()
1332- except OSError :
1333- # It is possible that the process gets completed in the duration
1334- # after timeout happens and before we try to kill the process.
1335- pass
1336- return
1337-
1338- def communicate () -> Tuple [AnyStr , AnyStr ]:
1339- watchdog .start ()
1340- out , err = proc .communicate ()
1341- watchdog .cancel ()
1342- if kill_check .is_set ():
1343- err = 'Timeout: the command "%s" did not complete in %d secs.' % (
1344- " " .join (redacted_command ),
1345- timeout ,
1346- )
1347- if not universal_newlines :
1348- err = err .encode (defenc )
1349- return out , err
1350-
1351- # END helpers
1352-
1353- kill_check = threading .Event ()
1354- watchdog = threading .Timer (timeout , kill_process , args = (proc .pid ,))
1355- else :
1356- communicate = proc .communicate
1357-
13581311 # Wait for the process to return.
13591312 status = 0
13601313 stdout_value : Union [str , bytes ] = b""
13611314 stderr_value : Union [str , bytes ] = b""
13621315 newline = "\n " if universal_newlines else b"\n "
13631316 try :
13641317 if output_stream is None :
1365- stdout_value , stderr_value = communicate ()
1318+ stdout_value , stderr_value = proc . communicate (timeout = kill_after_timeout )
13661319 # Strip trailing "\n".
13671320 if stdout_value is not None and stdout_value .endswith (newline ) and strip_newline_in_stdout : # type: ignore[arg-type]
13681321 stdout_value = stdout_value [:- 1 ]
@@ -1380,7 +1333,7 @@ def communicate() -> Tuple[AnyStr, AnyStr]:
13801333 # Strip trailing "\n".
13811334 if stderr_value is not None and stderr_value .endswith (newline ): # type: ignore[arg-type]
13821335 stderr_value = stderr_value [:- 1 ]
1383- status = proc .wait ()
1336+ status = proc .wait (timeout = kill_after_timeout )
13841337 # END stdout handling
13851338 finally :
13861339 if proc .stdout is not None :
0 commit comments