|
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', 'timeout') |
45 | 46 |
|
46 | 47 | log = logging.getLogger('git.cmd')
|
47 | 48 | log.addHandler(logging.NullHandler())
|
@@ -475,6 +476,7 @@ def execute(self, command,
|
475 | 476 | as_process=False,
|
476 | 477 | output_stream=None,
|
477 | 478 | stdout_as_string=True,
|
| 479 | + timeout=None, |
478 | 480 | with_stdout=True,
|
479 | 481 | **subprocess_kwargs
|
480 | 482 | ):
|
@@ -531,6 +533,12 @@ def execute(self, command,
|
531 | 533 |
|
532 | 534 | :param with_stdout: If True, default True, we open stdout on the created process
|
533 | 535 |
|
| 536 | + :param timeout: |
| 537 | + To specify a timeout in seconds for the git command, after which the process |
| 538 | + should be killed. This will have no effect if as_process is set to True. It is |
| 539 | + set to None by default and will let the process run until the timeout is |
| 540 | + explicitly specified. |
| 541 | +
|
534 | 542 | :return:
|
535 | 543 | * str(output) if extended_output = False (Default)
|
536 | 544 | * tuple(int(status), str(stdout), str(stderr)) if extended_output = True
|
@@ -592,13 +600,43 @@ def execute(self, command,
|
592 | 600 | if as_process:
|
593 | 601 | return self.AutoInterrupt(proc, command)
|
594 | 602 |
|
| 603 | + kill_check = threading.Event() |
| 604 | + |
| 605 | + def _kill_process(pid): |
| 606 | + """ Callback method to kill a process. """ |
| 607 | + p = Popen(['ps', '--ppid', str(pid)], stdout=PIPE) |
| 608 | + child_pids = [] |
| 609 | + for line in p.stdout: |
| 610 | + if len(line.split()) > 0: |
| 611 | + local_pid = (line.split())[0] |
| 612 | + if local_pid.isdigit(): |
| 613 | + child_pids.append(int(local_pid)) |
| 614 | + try: |
| 615 | + os.kill(pid, SIGKILL) |
| 616 | + for child_pid in child_pids: |
| 617 | + os.kill(child_pid, SIGKILL) |
| 618 | + kill_check.set() # tell the main routine that the process was killed |
| 619 | + except OSError: |
| 620 | + # It is possible that the process gets completed in the duration after timeout |
| 621 | + # happens and before we try to kill the process. |
| 622 | + pass |
| 623 | + return |
| 624 | + # end |
| 625 | + |
| 626 | + watchdog = threading.Timer(timeout, _kill_process, args=(proc.pid, )) |
| 627 | + |
595 | 628 | # Wait for the process to return
|
596 | 629 | status = 0
|
597 | 630 | stdout_value = b''
|
598 | 631 | stderr_value = b''
|
599 | 632 | try:
|
600 | 633 | if output_stream is None:
|
| 634 | + watchdog.start() |
601 | 635 | stdout_value, stderr_value = proc.communicate()
|
| 636 | + watchdog.cancel() |
| 637 | + if kill_check.isSet(): |
| 638 | + stderr_value = 'Timeout: the command "%s" did not complete in %d ' \ |
| 639 | + 'secs.' % (" ".join(command), timeout) |
602 | 640 | # strip trailing "\n"
|
603 | 641 | if stdout_value.endswith(b"\n"):
|
604 | 642 | stdout_value = stdout_value[:-1]
|
|
0 commit comments