mirror of https://github.com/fail2ban/fail2ban
Merge pull request #1136 from yarikoptic/bf-timeout-nested-commands-killpg
WiP BF: kill the entire process group upon timeout (Close #1129)pull/1143/head
commit
cb101e9f4a
|
@ -565,7 +565,9 @@ class CommandAction(ActionBase):
|
||||||
stderr = tempfile.TemporaryFile(suffix=".stderr", prefix="fai2ban_")
|
stderr = tempfile.TemporaryFile(suffix=".stderr", prefix="fai2ban_")
|
||||||
try:
|
try:
|
||||||
popen = subprocess.Popen(
|
popen = subprocess.Popen(
|
||||||
realCmd, stdout=stdout, stderr=stderr, shell=True)
|
realCmd, stdout=stdout, stderr=stderr, shell=True,
|
||||||
|
preexec_fn=os.setsid # so that killpg does not kill our process
|
||||||
|
)
|
||||||
stime = time.time()
|
stime = time.time()
|
||||||
retcode = popen.poll()
|
retcode = popen.poll()
|
||||||
while time.time() - stime <= timeout and retcode is None:
|
while time.time() - stime <= timeout and retcode is None:
|
||||||
|
@ -574,11 +576,12 @@ class CommandAction(ActionBase):
|
||||||
if retcode is None:
|
if retcode is None:
|
||||||
logSys.error("%s -- timed out after %i seconds." %
|
logSys.error("%s -- timed out after %i seconds." %
|
||||||
(realCmd, timeout))
|
(realCmd, timeout))
|
||||||
os.kill(popen.pid, signal.SIGTERM) # Terminate the process
|
pgid = os.getpgid(popen.pid)
|
||||||
|
os.killpg(pgid, signal.SIGTERM) # Terminate the process
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
retcode = popen.poll()
|
retcode = popen.poll()
|
||||||
if retcode is None: # Still going...
|
if retcode is None: # Still going...
|
||||||
os.kill(popen.pid, signal.SIGKILL) # Kill the process
|
os.killpg(pgid, signal.SIGKILL) # Kill the process
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
retcode = popen.poll()
|
retcode = popen.poll()
|
||||||
except OSError, e:
|
except OSError, e:
|
||||||
|
|
|
@ -24,12 +24,14 @@ __author__ = "Cyril Jaquier"
|
||||||
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
__copyright__ = "Copyright (c) 2004 Cyril Jaquier"
|
||||||
__license__ = "GPL"
|
__license__ = "GPL"
|
||||||
|
|
||||||
|
import os
|
||||||
import time
|
import time
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from ..server.action import CommandAction, CallingMap
|
from ..server.action import CommandAction, CallingMap
|
||||||
|
|
||||||
from .utils import LogCaptureTestCase
|
from .utils import LogCaptureTestCase
|
||||||
|
from .utils import pid_exists
|
||||||
|
|
||||||
class CommandActionTest(LogCaptureTestCase):
|
class CommandActionTest(LogCaptureTestCase):
|
||||||
|
|
||||||
|
@ -202,6 +204,44 @@ class CommandActionTest(LogCaptureTestCase):
|
||||||
or self._is_logged('sleep 60 -- timed out after 3 seconds'))
|
or self._is_logged('sleep 60 -- timed out after 3 seconds'))
|
||||||
self.assertTrue(self._is_logged('sleep 60 -- killed with SIGTERM'))
|
self.assertTrue(self._is_logged('sleep 60 -- killed with SIGTERM'))
|
||||||
|
|
||||||
|
def testExecuteTimeoutWithNastyChildren(self):
|
||||||
|
# temporary file for a nasty kid shell script
|
||||||
|
tmpFilename = tempfile.mktemp(".sh", "fail2ban_")
|
||||||
|
# Create a nasty script which would hang there for a while
|
||||||
|
with open(tmpFilename, 'w') as f:
|
||||||
|
f.write("""#!/bin/bash
|
||||||
|
trap : HUP EXIT TERM
|
||||||
|
|
||||||
|
echo "$$" > %s.pid
|
||||||
|
echo "my pid $$ . sleeping lo-o-o-ong"
|
||||||
|
sleep 10000
|
||||||
|
""" % tmpFilename)
|
||||||
|
|
||||||
|
def getnastypid():
|
||||||
|
with open(tmpFilename + '.pid') as f:
|
||||||
|
return int(f.read())
|
||||||
|
|
||||||
|
# First test if can kill the bastard
|
||||||
|
self.assertRaises(
|
||||||
|
RuntimeError, CommandAction.executeCmd, 'bash %s' % tmpFilename, timeout=.1)
|
||||||
|
# Verify that the proccess itself got killed
|
||||||
|
self.assertFalse(pid_exists(getnastypid())) # process should have been killed
|
||||||
|
self.assertTrue(self._is_logged('timed out'))
|
||||||
|
self.assertTrue(self._is_logged('killed with SIGTERM'))
|
||||||
|
|
||||||
|
# A bit evolved case even though, previous test already tests killing children processes
|
||||||
|
self.assertRaises(
|
||||||
|
RuntimeError, CommandAction.executeCmd, 'out=`bash %s`; echo ALRIGHT' % tmpFilename,
|
||||||
|
timeout=.2)
|
||||||
|
# Verify that the proccess itself got killed
|
||||||
|
self.assertFalse(pid_exists(getnastypid()))
|
||||||
|
self.assertTrue(self._is_logged('timed out'))
|
||||||
|
self.assertTrue(self._is_logged('killed with SIGTERM'))
|
||||||
|
|
||||||
|
os.unlink(tmpFilename)
|
||||||
|
os.unlink(tmpFilename + '.pid')
|
||||||
|
|
||||||
|
|
||||||
def testCaptureStdOutErr(self):
|
def testCaptureStdOutErr(self):
|
||||||
CommandAction.executeCmd('echo "How now brown cow"')
|
CommandAction.executeCmd('echo "How now brown cow"')
|
||||||
self.assertTrue(self._is_logged("'How now brown cow\\n'"))
|
self.assertTrue(self._is_logged("'How now brown cow\\n'"))
|
||||||
|
|
|
@ -237,3 +237,30 @@ class LogCaptureTestCase(unittest.TestCase):
|
||||||
|
|
||||||
def printLog(self):
|
def printLog(self):
|
||||||
print(self._log.getvalue())
|
print(self._log.getvalue())
|
||||||
|
|
||||||
|
# Solution from http://stackoverflow.com/questions/568271/how-to-check-if-there-exists-a-process-with-a-given-pid
|
||||||
|
# under cc by-sa 3.0
|
||||||
|
if os.name == 'posix':
|
||||||
|
def pid_exists(pid):
|
||||||
|
"""Check whether pid exists in the current process table."""
|
||||||
|
import errno
|
||||||
|
if pid < 0:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
os.kill(pid, 0)
|
||||||
|
except OSError as e:
|
||||||
|
return e.errno == errno.EPERM
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
def pid_exists(pid):
|
||||||
|
import ctypes
|
||||||
|
kernel32 = ctypes.windll.kernel32
|
||||||
|
SYNCHRONIZE = 0x100000
|
||||||
|
|
||||||
|
process = kernel32.OpenProcess(SYNCHRONIZE, 0, pid)
|
||||||
|
if process != 0:
|
||||||
|
kernel32.CloseHandle(process)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
Loading…
Reference in New Issue