1414import mmap
1515
1616from contextlib import contextmanager
17+ from signal import SIGKILL
1718from subprocess import (
1819 call ,
1920 Popen ,
4142
4243execute_kwargs = ('istream' , 'with_keep_cwd' , 'with_extended_output' ,
4344 'with_exceptions' , 'as_process' , 'stdout_as_string' ,
44- 'output_stream' , 'with_stdout' )
45+ 'output_stream' , 'with_stdout' , 'kill_after_timeout' )
4546
4647log = logging .getLogger ('git.cmd' )
4748log .addHandler (logging .NullHandler ())
@@ -476,6 +477,7 @@ def execute(self, command,
476477 as_process = False ,
477478 output_stream = None ,
478479 stdout_as_string = True ,
480+ kill_after_timeout = None ,
479481 with_stdout = True ,
480482 ** subprocess_kwargs
481483 ):
@@ -532,6 +534,16 @@ def execute(self, command,
532534
533535 :param with_stdout: If True, default True, we open stdout on the created process
534536
537+ :param kill_after_timeout:
538+ To specify a timeout in seconds for the git command, after which the process
539+ should be killed. This will have no effect if as_process is set to True. It is
540+ set to None by default and will let the process run until the timeout is
541+ explicitly specified. This feature is not supported on Windows. It's also worth
542+ noting that kill_after_timeout uses SIGKILL, which can have negative side
543+ effects on a repository. For example, stale locks in case of git gc could
544+ render the repository incapable of accepting changes until the lock is manually
545+ removed.
546+
535547 :return:
536548 * str(output) if extended_output = False (Default)
537549 * tuple(int(status), str(stdout), str(stderr)) if extended_output = True
@@ -569,6 +581,8 @@ def execute(self, command,
569581
570582 if sys .platform == 'win32' :
571583 cmd_not_found_exception = WindowsError
584+ if kill_after_timeout :
585+ raise GitCommandError ('"kill_after_timeout" feature is not supported on Windows.' )
572586 else :
573587 if sys .version_info [0 ] > 2 :
574588 cmd_not_found_exception = FileNotFoundError # NOQA # this is defined, but flake8 doesn't know
@@ -593,13 +607,48 @@ def execute(self, command,
593607 if as_process :
594608 return self .AutoInterrupt (proc , command )
595609
610+ def _kill_process (pid ):
611+ """ Callback method to kill a process. """
612+ p = Popen (['ps' , '--ppid' , str (pid )], stdout = PIPE )
613+ child_pids = []
614+ for line in p .stdout :
615+ if len (line .split ()) > 0 :
616+ local_pid = (line .split ())[0 ]
617+ if local_pid .isdigit ():
618+ child_pids .append (int (local_pid ))
619+ try :
620+ os .kill (pid , SIGKILL )
621+ for child_pid in child_pids :
622+ try :
623+ os .kill (child_pid , SIGKILL )
624+ except OSError :
625+ pass
626+ kill_check .set () # tell the main routine that the process was killed
627+ except OSError :
628+ # It is possible that the process gets completed in the duration after timeout
629+ # happens and before we try to kill the process.
630+ pass
631+ return
632+ # end
633+
634+ if kill_after_timeout :
635+ kill_check = threading .Event ()
636+ watchdog = threading .Timer (kill_after_timeout , _kill_process , args = (proc .pid , ))
637+
596638 # Wait for the process to return
597639 status = 0
598640 stdout_value = b''
599641 stderr_value = b''
600642 try :
601643 if output_stream is None :
644+ if kill_after_timeout :
645+ watchdog .start ()
602646 stdout_value , stderr_value = proc .communicate ()
647+ if kill_after_timeout :
648+ watchdog .cancel ()
649+ if kill_check .isSet ():
650+ stderr_value = 'Timeout: the command "%s" did not complete in %d ' \
651+ 'secs.' % (" " .join (command ), kill_after_timeout )
603652 # strip trailing "\n"
604653 if stdout_value .endswith (b"\n " ):
605654 stdout_value = stdout_value [:- 1 ]
0 commit comments