summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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):