Skip to content

Commit a929ab2

Browse files
committed
Merge pull request #354 from dpursehouse/execute-timeout
Include 'timeout' parameter in Git execute
2 parents ed8939d + dbbcaf7 commit a929ab2

File tree

1 file changed

+50
-1
lines changed

1 file changed

+50
-1
lines changed

‎git/cmd.py‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
importmmap
1515

1616
fromcontextlibimportcontextmanager
17+
fromsignalimportSIGKILL
1718
fromsubprocessimport (
1819
call,
1920
Popen,
@@ -41,7 +42,7 @@
4142

4243
execute_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

4647
log=logging.getLogger('git.cmd')
4748
log.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
ifsys.platform=='win32':
571583
cmd_not_found_exception=WindowsError
584+
ifkill_after_timeout:
585+
raiseGitCommandError('"kill_after_timeout" feature is not supported on Windows.')
572586
else:
573587
ifsys.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
ifas_process:
594608
returnself.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+
forlineinp.stdout:
615+
iflen(line.split()) >0:
616+
local_pid= (line.split())[0]
617+
iflocal_pid.isdigit():
618+
child_pids.append(int(local_pid))
619+
try:
620+
os.kill(pid, SIGKILL)
621+
forchild_pidinchild_pids:
622+
try:
623+
os.kill(child_pid, SIGKILL)
624+
exceptOSError:
625+
pass
626+
kill_check.set() # tell the main routine that the process was killed
627+
exceptOSError:
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+
ifkill_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
ifoutput_streamisNone:
644+
ifkill_after_timeout:
645+
watchdog.start()
602646
stdout_value, stderr_value=proc.communicate()
647+
ifkill_after_timeout:
648+
watchdog.cancel()
649+
ifkill_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
ifstdout_value.endswith(b"\n"):
605654
stdout_value=stdout_value[:-1]

0 commit comments

Comments
(0)