14
14
import mmap
15
15
16
16
from contextlib import contextmanager
17
+ from signal import SIGKILL
17
18
from subprocess import (
18
19
call ,
19
20
Popen ,
41
42
42
43
execute_kwargs = ('istream' , 'with_keep_cwd' , 'with_extended_output' ,
43
44
'with_exceptions' , 'as_process' , 'stdout_as_string' ,
44
- 'output_stream' , 'with_stdout' )
45
+ 'output_stream' , 'with_stdout' , 'kill_after_timeout' )
45
46
46
47
log = logging .getLogger ('git.cmd' )
47
48
log .addHandler (logging .NullHandler ())
@@ -476,6 +477,7 @@ def execute(self, command,
476
477
as_process = False ,
477
478
output_stream = None ,
478
479
stdout_as_string = True ,
480
+ kill_after_timeout = None ,
479
481
with_stdout = True ,
480
482
** subprocess_kwargs
481
483
):
@@ -532,6 +534,16 @@ def execute(self, command,
532
534
533
535
:param with_stdout: If True, default True, we open stdout on the created process
534
536
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
+
535
547
:return:
536
548
* str(output) if extended_output = False (Default)
537
549
* tuple(int(status), str(stdout), str(stderr)) if extended_output = True
@@ -569,6 +581,8 @@ def execute(self, command,
569
581
570
582
if sys .platform == 'win32' :
571
583
cmd_not_found_exception = WindowsError
584
+ if kill_after_timeout :
585
+ raise GitCommandError ('"kill_after_timeout" feature is not supported on Windows.' )
572
586
else :
573
587
if sys .version_info [0 ] > 2 :
574
588
cmd_not_found_exception = FileNotFoundError # NOQA # this is defined, but flake8 doesn't know
@@ -593,13 +607,48 @@ def execute(self, command,
593
607
if as_process :
594
608
return self .AutoInterrupt (proc , command )
595
609
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
+
596
638
# Wait for the process to return
597
639
status = 0
598
640
stdout_value = b''
599
641
stderr_value = b''
600
642
try :
601
643
if output_stream is None :
644
+ if kill_after_timeout :
645
+ watchdog .start ()
602
646
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 )
603
652
# strip trailing "\n"
604
653
if stdout_value .endswith (b"\n " ):
605
654
stdout_value = stdout_value [:- 1 ]
0 commit comments