summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMehdi ABAAKOUK <sileht@sileht.net>2020-01-09 15:57:43 +0100
committerBernát Gábor <bgabor8@bloomberg.net>2020-01-09 14:57:43 +0000
commite639a54eab5641c8dde353a29a516e4bf5487e37 (patch)
tree9d31601efcea78c8e6d1cbd674f50e80ddddf5e5
parent6472eac8fc4f0c222d9f40c4572aafbca72c40db (diff)
downloadtox-git-e639a54eab5641c8dde353a29a516e4bf5487e37.tar.gz
Allow to config the behavior on CTRL+C (#1493)
By default, we run SIGKILL after 0.5 seconds. Most of the time is enough. But if the interrupted command have a complex processes tree, it might not be enough to propagate the signal. In such case processes are left behind and never killed. If theses processes use static network port or keep file open. Next call of tox will fail until the all processes left behind are manually killed. This change adds some configuration to be able to config the timeout before signals are sent. If the approach work for you, I will polish the PR (doc+test)
-rw-r--r--CONTRIBUTORS1
-rw-r--r--docs/changelog/1493.feature.rst1
-rw-r--r--docs/config.rst15
-rw-r--r--src/tox/action.py25
-rw-r--r--src/tox/config/__init__.py42
-rw-r--r--src/tox/session/__init__.py4
-rw-r--r--src/tox/venv.py2
-rw-r--r--tests/unit/config/test_config.py19
8 files changed, 100 insertions, 9 deletions
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index f944ddb2..5701b5b1 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -57,6 +57,7 @@ Mark Hirota
Matt Good
Matt Jeffery
Mattieu Agopian
+Mehdi Abaakouk
Michael Manganiello
Mickaël Schoentgen
Mikhail Kyshtymov
diff --git a/docs/changelog/1493.feature.rst b/docs/changelog/1493.feature.rst
new file mode 100644
index 00000000..20f8fb30
--- /dev/null
+++ b/docs/changelog/1493.feature.rst
@@ -0,0 +1 @@
+Add ``interrupt_timeout`` and ``terminate_timeout`` that configure delay between SIGINT, SIGTERM and SIGKILL when tox is interrupted. - by :user:`sileht`
diff --git a/docs/config.rst b/docs/config.rst
index 8d9dbb74..4eba670c 100644
--- a/docs/config.rst
+++ b/docs/config.rst
@@ -160,6 +160,21 @@ Global settings are defined under the ``tox`` section as:
Name of the virtual environment used to create a source distribution from the
source tree.
+.. conf:: interrupt_timeout ^ float ^ 0.3
+
+ .. versionadded:: 3.15.0
+
+ When tox is interrupted, it propagates the signal to the child process,
+ wait :conf:``interrupt_timeout`` seconds, and sends it a SIGTERM if it haven't
+ exited.
+
+.. conf:: terminate_timeout ^ float ^ 0.2
+
+ .. versionadded:: 3.15.0
+
+ When tox is interrupted, it propagates the signal to the child process,
+ wait :conf:``interrupt_timeout`` seconds, sends it a SIGTERM, wait
+ :conf:``terminate_timeout`` seconds, and sends it a SIGKILL if it haven't exited.
Jenkins override
++++++++++++++++
diff --git a/src/tox/action.py b/src/tox/action.py
index 10707b48..6dae0d07 100644
--- a/src/tox/action.py
+++ b/src/tox/action.py
@@ -18,14 +18,23 @@ from tox.reporter import Verbosity
from tox.util.lock import get_unique_file
from tox.util.stdlib import is_main_thread
-WAIT_INTERRUPT = 0.3
-WAIT_TERMINATE = 0.2
-
class Action(object):
"""Action is an effort to group operations with the same goal (within reporting)"""
- def __init__(self, name, msg, args, log_dir, generate_tox_log, command_log, popen, python):
+ def __init__(
+ self,
+ name,
+ msg,
+ args,
+ log_dir,
+ generate_tox_log,
+ command_log,
+ popen,
+ python,
+ interrupt_timeout,
+ terminate_timeout,
+ ):
self.name = name
self.args = args
self.msg = msg
@@ -36,6 +45,8 @@ class Action(object):
self.command_log = command_log
self._timed_report = None
self.python = python
+ self.interrupt_timeout = interrupt_timeout
+ self.terminate_timeout = terminate_timeout
def __enter__(self):
msg = "{} {}".format(self.msg, " ".join(map(str, self.args)))
@@ -180,10 +191,10 @@ class Action(object):
if process.poll() is None:
self.info("KeyboardInterrupt", msg.format("SIGINT"))
process.send_signal(signal.CTRL_C_EVENT if sys.platform == "win32" else signal.SIGINT)
- if self._wait(process, WAIT_INTERRUPT) is None:
+ if self._wait(process, self.interrupt_timeout) is None:
self.info("KeyboardInterrupt", msg.format("SIGTERM"))
process.terminate()
- if self._wait(process, WAIT_TERMINATE) is None:
+ if self._wait(process, self.terminate_timeout) is None:
self.info("KeyboardInterrupt", msg.format("SIGKILL"))
process.kill()
process.communicate()
@@ -193,7 +204,7 @@ class Action(object):
if sys.version_info >= (3, 3):
# python 3 has timeout feature built-in
try:
- process.communicate(timeout=WAIT_INTERRUPT)
+ process.communicate(timeout=timeout)
except subprocess.TimeoutExpired:
pass
else:
diff --git a/src/tox/config/__init__.py b/src/tox/config/__init__.py
index d80da5ff..3b612676 100644
--- a/src/tox/config/__init__.py
+++ b/src/tox/config/__init__.py
@@ -54,6 +54,9 @@ Import hookimpl directly from tox instead.
WITHIN_PROVISION = os.environ.get(str("TOX_PROVISION")) == "1"
+INTERRUPT_TIMEOUT = 0.3
+TERMINATE_TIMEOUT = 0.2
+
def get_plugin_manager(plugins=()):
# initialize plugin manager
@@ -799,6 +802,20 @@ def tox_addoption(parser):
parser.add_testenv_attribute_obj(DepOption())
parser.add_testenv_attribute(
+ name="interrupt_timeout",
+ type="float",
+ default=INTERRUPT_TIMEOUT,
+ help="timeout before sending SIGTERM after SIGINT",
+ )
+
+ parser.add_testenv_attribute(
+ name="terminate_timeout",
+ type="float",
+ default=TERMINATE_TIMEOUT,
+ help="timeout before sending SIGKILL after SIGTERM",
+ )
+
+ parser.add_testenv_attribute(
name="commands",
type="argvlist",
default="",
@@ -1231,7 +1248,16 @@ class ParseIni(object):
for env_attr in config._testenv_attr:
atype = env_attr.type
try:
- if atype in ("bool", "path", "string", "dict", "dict_setenv", "argv", "argvlist"):
+ if atype in (
+ "bool",
+ "float",
+ "path",
+ "string",
+ "dict",
+ "dict_setenv",
+ "argv",
+ "argvlist",
+ ):
meth = getattr(reader, "get{}".format(atype))
res = meth(env_attr.name, env_attr.default, replace=replace)
elif atype == "basepython":
@@ -1448,6 +1474,20 @@ class SectionReader:
return d
+ def getfloat(self, name, default=None, replace=True):
+ s = self.getstring(name, default, replace=replace)
+ if not s or not replace:
+ s = default
+ if s is None:
+ raise KeyError("no config value [{}] {} found".format(self.section_name, name))
+
+ if not isinstance(s, float):
+ try:
+ s = float(s)
+ except ValueError:
+ raise tox.exception.ConfigError("{}: invalid float {!r}".format(name, s))
+ return s
+
def getbool(self, name, default=None, replace=True):
s = self.getstring(name, default, replace=replace)
if not s or not replace:
diff --git a/src/tox/session/__init__.py b/src/tox/session/__init__.py
index b2e901e1..a2e0ca78 100644
--- a/src/tox/session/__init__.py
+++ b/src/tox/session/__init__.py
@@ -19,7 +19,7 @@ import py
import tox
from tox import reporter
from tox.action import Action
-from tox.config import parseconfig
+from tox.config import INTERRUPT_TIMEOUT, TERMINATE_TIMEOUT, parseconfig
from tox.config.parallel import ENV_VAR_KEY_PRIVATE as PARALLEL_ENV_VAR_KEY_PRIVATE
from tox.config.parallel import OFF_VALUE as PARALLEL_OFF
from tox.logs.result import ResultLog
@@ -170,6 +170,8 @@ class Session(object):
self.resultlog.command_log,
self.popen,
sys.executable,
+ INTERRUPT_TIMEOUT,
+ TERMINATE_TIMEOUT,
)
def runcommand(self):
diff --git a/src/tox/venv.py b/src/tox/venv.py
index 4edf741b..0cd78c24 100644
--- a/src/tox/venv.py
+++ b/src/tox/venv.py
@@ -130,6 +130,8 @@ class VirtualEnv(object):
command_log,
self.popen,
self.envconfig.envpython,
+ self.envconfig.interrupt_timeout,
+ self.envconfig.terminate_timeout,
)
def get_result_json_path(self):
diff --git a/tests/unit/config/test_config.py b/tests/unit/config/test_config.py
index 9fd5b23c..a717b8c3 100644
--- a/tests/unit/config/test_config.py
+++ b/tests/unit/config/test_config.py
@@ -175,6 +175,25 @@ class TestVenvConfig:
assert DepOption._is_same_dep("pkg_hello-world3==1.0", "pkg_hello-world3<=2.0")
assert not DepOption._is_same_dep("pkg_hello-world3==1.0", "otherpkg>=2.0")
+ def test_interrupt_terminate_timeout_set_manually(self, newconfig):
+ config = newconfig(
+ [],
+ """
+ [testenv:dev]
+ interrupt_timeout = 5.0
+ terminate_timeout = 10.0
+
+ [testenv:other]
+ """,
+ )
+ envconfig = config.envconfigs["other"]
+ assert 0.3 == envconfig.interrupt_timeout
+ assert 0.2 == envconfig.terminate_timeout
+
+ envconfig = config.envconfigs["dev"]
+ assert 5.0 == envconfig.interrupt_timeout
+ assert 10.0 == envconfig.terminate_timeout
+
class TestConfigPlatform:
def test_config_parse_platform(self, newconfig):