Skip to content

Include 'timeout' parameter in Git execute #354

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 16, 2015
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion git/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import mmap

from contextlib import contextmanager
from signal import SIGKILL
from subprocess import (
call,
Popen,
Expand Down Expand Up @@ -41,7 +42,7 @@

execute_kwargs = ('istream', 'with_keep_cwd', 'with_extended_output',
'with_exceptions', 'as_process', 'stdout_as_string',
'output_stream', 'with_stdout')
'output_stream', 'with_stdout', 'kill_after_timeout')

log = logging.getLogger('git.cmd')
log.addHandler(logging.NullHandler())
Expand Down Expand Up @@ -475,6 +476,7 @@ def execute(self, command,
as_process=False,
output_stream=None,
stdout_as_string=True,
kill_after_timeout=None,
with_stdout=True,
**subprocess_kwargs
):
Expand Down Expand Up @@ -531,6 +533,16 @@ def execute(self, command,

:param with_stdout: If True, default True, we open stdout on the created process

:param kill_after_timeout:
To specify a timeout in seconds for the git command, after which the process
should be killed. This will have no effect if as_process is set to True. It is
set to None by default and will let the process run until the timeout is
explicitly specified. This feature is not supported on Windows. It's also worth
noting that kill_after_timeout uses SIGKILL, which can have negative side
effects on a repository. For example, stale locks in case of git gc could
render the repository incapable of accepting changes until the lock is manually
removed.

:return:
* str(output) if extended_output = False (Default)
* tuple(int(status), str(stdout), str(stderr)) if extended_output = True
Expand Down Expand Up @@ -568,6 +580,8 @@ def execute(self, command,

if sys.platform == 'win32':
cmd_not_found_exception = WindowsError
if kill_after_timeout:
raise GitCommandError('"kill_after_timeout" feature is not supported on Windows.')
else:
if sys.version_info[0] > 2:
cmd_not_found_exception = FileNotFoundError # NOQA # this is defined, but flake8 doesn't know
Expand All @@ -592,13 +606,48 @@ def execute(self, command,
if as_process:
return self.AutoInterrupt(proc, command)

def _kill_process(pid):
""" Callback method to kill a process. """
p = Popen(['ps', '--ppid', str(pid)], stdout=PIPE)
child_pids = []
for line in p.stdout:
if len(line.split()) > 0:
local_pid = (line.split())[0]
if local_pid.isdigit():
child_pids.append(int(local_pid))
try:
os.kill(pid, SIGKILL)
for child_pid in child_pids:
try:
os.kill(child_pid, SIGKILL)
except OSError:
pass
kill_check.set() # tell the main routine that the process was killed
except OSError:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If os.kill throws OSError if the process to be killed doesn't exist, I fear that after os.kill(pid, SIGKILL), some of the child processes might already be down as pid. Right now, failure to kill the first child process, will prevent the others from being downed as well. Maybe something like that would help:

for child_pid in child_pids:
    try:
        os.kill(child_pid, SIGKILL)
    except OSError:
        pass

# It is possible that the process gets completed in the duration after timeout
# happens and before we try to kill the process.
pass
return
# end

if kill_after_timeout:
kill_check = threading.Event()
watchdog = threading.Timer(kill_after_timeout, _kill_process, args=(proc.pid, ))

# Wait for the process to return
status = 0
stdout_value = b''
stderr_value = b''
try:
if output_stream is None:
if kill_after_timeout:
watchdog.start()
stdout_value, stderr_value = proc.communicate()
if kill_after_timeout:
watchdog.cancel()
if kill_check.isSet():
stderr_value = 'Timeout: the command "%s" did not complete in %d ' \
'secs.' % (" ".join(command), kill_after_timeout)
# strip trailing "\n"
if stdout_value.endswith(b"\n"):
stdout_value = stdout_value[:-1]
Expand Down