1313import logging
1414import os
1515import re
16- import signal
1716import subprocess
1817from subprocess import DEVNULL , PIPE , Popen
1918import sys
@@ -110,7 +109,7 @@ def handle_process_output(
110109 stderr_handler : Union [None , Callable [[AnyStr ], None ], Callable [[List [AnyStr ]], None ]],
111110 finalizer : Union [None , Callable [[Union [Popen , "Git.AutoInterrupt" ]], None ]] = None ,
112111 decode_streams : bool = True ,
113- kill_after_timeout : Union [ None , float ] = None ,
112+ kill_after_timeout : float | None = None ,
114113) -> None :
115114 R"""Register for notifications to learn that process output is ready to read, and
116115 dispatch lines to the respective line handlers.
@@ -139,7 +138,7 @@ def handle_process_output(
139138 - decoding must happen later, such as for :class:`~git.diff.Diff`\s.
140139
141140 :param kill_after_timeout:
142- :class:`float` or ``None``, Default = ``None``
141+ :class:``int``, `` float``, or ``None`` (block indefinitely) , Default = ``None``.
143142
144143 To specify a timeout in seconds for the git command, after which the process
145144 should be killed.
@@ -326,16 +325,22 @@ class _AutoInterrupt:
326325 raise.
327326 """
328327
329- __slots__ = ("proc" , "args" , "status" )
328+ __slots__ = ("proc" , "args" , "status" , "timeout" )
330329
331330 # If this is non-zero it will override any status code during _terminate, used
332331 # to prevent race conditions in testing.
333332 _status_code_if_terminate : int = 0
334333
335- def __init__ (self , proc : Union [None , subprocess .Popen ], args : Any ) -> None :
334+ def __init__ (
335+ self ,
336+ proc : subprocess .Popen | None ,
337+ args : Any ,
338+ timeout : float | 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,10 +370,10 @@ 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
371- except (OSError , AttributeError ) as ex :
376+ except (OSError , AttributeError , subprocess . TimeoutExpired ) as ex :
372377 # On interpreter shutdown (notably on Windows), parts of the stdlib used by
373378 # subprocess can already be torn down (e.g. `subprocess._winapi` becomes None),
374379 # which can cause AttributeError during terminate(). In that case, we prefer
@@ -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
@@ -1106,7 +1111,7 @@ def execute(
11061111 as_process : bool = False ,
11071112 output_stream : Union [None , BinaryIO ] = None ,
11081113 stdout_as_string : bool = True ,
1109- kill_after_timeout : Union [ None , float ] = None ,
1114+ kill_after_timeout : float | None = None ,
11101115 with_stdout : bool = True ,
11111116 universal_newlines : bool = False ,
11121117 shell : Union [None , bool ] = None ,
@@ -1158,13 +1163,12 @@ def execute(
11581163 until the timeout is explicitly specified. Uses of this feature should be
11591164 carefully considered, due to the following limitations:
11601165
1161- 1. This feature is not supported at all on Windows.
1162- 2. Effectiveness may vary by operating system. ``ps --ppid`` is used to
1166+ 1. Effectiveness may vary by operating system. ``ps --ppid`` is used to
11631167 enumerate child processes, which is available on most GNU/Linux systems
11641168 but not most others.
1165- 3 . Deeper descendants do not receive signals, though they may sometimes
1169+ 2 . Deeper descendants do not receive signals, though they may sometimes
11661170 terminate as a consequence of their parent processes being killed.
1167- 4 . `kill_after_timeout` uses ``SIGKILL``, which can have negative side
1171+ 3 . `kill_after_timeout` uses ``SIGKILL``, which can have negative side
11681172 effects on a repository. For example, stale locks in case of
11691173 :manpage:`git-gc(1)` could render the repository incapable of accepting
11701174 changes until the lock is manually removed.
@@ -1252,15 +1256,7 @@ def execute(
12521256 if inline_env is not None :
12531257 env .update (inline_env )
12541258
1255- if sys .platform == "win32" :
1256- if kill_after_timeout is not None :
1257- raise GitCommandError (
1258- redacted_command ,
1259- '"kill_after_timeout" feature is not supported on Windows.' ,
1260- )
1261- cmd_not_found_exception = OSError
1262- else :
1263- cmd_not_found_exception = FileNotFoundError
1259+ cmd_not_found_exception = OSError if sys .platform == "win32" else FileNotFoundError
12641260 # END handle
12651261
12661262 stdout_sink = PIPE if with_stdout else getattr (subprocess , "DEVNULL" , None ) or open (os .devnull , "wb" )
@@ -1303,66 +1299,14 @@ def execute(
13031299 if as_process :
13041300 return self .AutoInterrupt (proc , command )
13051301
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-
13581302 # Wait for the process to return.
13591303 status = 0
13601304 stdout_value : Union [str , bytes ] = b""
13611305 stderr_value : Union [str , bytes ] = b""
13621306 newline = "\n " if universal_newlines else b"\n "
13631307 try :
13641308 if output_stream is None :
1365- stdout_value , stderr_value = communicate ()
1309+ stdout_value , stderr_value = proc . communicate (timeout = kill_after_timeout )
13661310 # Strip trailing "\n".
13671311 if stdout_value is not None and stdout_value .endswith (newline ) and strip_newline_in_stdout : # type: ignore[arg-type]
13681312 stdout_value = stdout_value [:- 1 ]
@@ -1380,8 +1324,15 @@ def communicate() -> Tuple[AnyStr, AnyStr]:
13801324 # Strip trailing "\n".
13811325 if stderr_value is not None and stderr_value .endswith (newline ): # type: ignore[arg-type]
13821326 stderr_value = stderr_value [:- 1 ]
1383- status = proc .wait ()
1327+ status = proc .wait (timeout = kill_after_timeout )
13841328 # END stdout handling
1329+ except subprocess .TimeoutExpired as err :
1330+ _logger .info (
1331+ "error: process killed because it timed out. kill_after_timeout=%s seconds" ,
1332+ kill_after_timeout ,
1333+ )
1334+ if with_exceptions :
1335+ raise GitCommandError (redacted_command , status or 255 , err .stderr , err .stdout ) from err
13851336 finally :
13861337 if proc .stdout is not None :
13871338 proc .stdout .close ()
0 commit comments