diff options
-rw-r--r-- | CONTRIBUTORS | 1 | ||||
-rw-r--r-- | docs/changelog/1493.feature.rst | 1 | ||||
-rw-r--r-- | docs/config.rst | 15 | ||||
-rw-r--r-- | src/tox/action.py | 25 | ||||
-rw-r--r-- | src/tox/config/__init__.py | 42 | ||||
-rw-r--r-- | src/tox/session/__init__.py | 4 | ||||
-rw-r--r-- | src/tox/venv.py | 2 | ||||
-rw-r--r-- | tests/unit/config/test_config.py | 19 |
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): |