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