Skip to content

Commit 39d4e2a

Browse files
committed
Allow to config the behavior on CTRL+C
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)
1 parent 6472eac commit 39d4e2a

File tree

4 files changed

+46
-8
lines changed

4 files changed

+46
-8
lines changed

src/tox/action.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,12 @@
1818
from tox.util.lock import get_unique_file
1919
from tox.util.stdlib import is_main_thread
2020

21-
WAIT_INTERRUPT = 0.3
22-
WAIT_TERMINATE = 0.2
23-
2421

2522
class Action(object):
2623
"""Action is an effort to group operations with the same goal (within reporting)"""
2724

28-
def __init__(self, name, msg, args, log_dir, generate_tox_log, command_log, popen, python):
25+
def __init__(self, name, msg, args, log_dir, generate_tox_log, command_log, popen,
26+
python, interrupt_timeout, terminate_timeout):
2927
self.name = name
3028
self.args = args
3129
self.msg = msg
@@ -36,6 +34,8 @@ def __init__(self, name, msg, args, log_dir, generate_tox_log, command_log, pope
3634
self.command_log = command_log
3735
self._timed_report = None
3836
self.python = python
37+
self.interrupt_timeout = interrupt_timeout
38+
self.terminate_timeout = terminate_timeout
3939

4040
def __enter__(self):
4141
msg = "{} {}".format(self.msg, " ".join(map(str, self.args)))
@@ -180,10 +180,10 @@ def handle_interrupt(self, process):
180180
if process.poll() is None:
181181
self.info("KeyboardInterrupt", msg.format("SIGINT"))
182182
process.send_signal(signal.CTRL_C_EVENT if sys.platform == "win32" else signal.SIGINT)
183-
if self._wait(process, WAIT_INTERRUPT) is None:
183+
if self._wait(process, self.interrupt_timeout) is None:
184184
self.info("KeyboardInterrupt", msg.format("SIGTERM"))
185185
process.terminate()
186-
if self._wait(process, WAIT_TERMINATE) is None:
186+
if self._wait(process, self.terminate_timeout) is None:
187187
self.info("KeyboardInterrupt", msg.format("SIGKILL"))
188188
process.kill()
189189
process.communicate()
@@ -193,7 +193,7 @@ def _wait(process, timeout):
193193
if sys.version_info >= (3, 3):
194194
# python 3 has timeout feature built-in
195195
try:
196-
process.communicate(timeout=WAIT_INTERRUPT)
196+
process.communicate(timeout=timeout)
197197
except subprocess.TimeoutExpired:
198198
pass
199199
else:

src/tox/config/__init__.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,20 @@ def develop(testenv_config, value):
798798

799799
parser.add_testenv_attribute_obj(DepOption())
800800

801+
parser.add_testenv_attribute(
802+
name="interrupt_timeout",
803+
type="float",
804+
default=0.3,
805+
help="timeout before sending SIGTERM after SIGINT",
806+
)
807+
808+
parser.add_testenv_attribute(
809+
name="terminate_timeout",
810+
type="float",
811+
default=0.2,
812+
help="timeout before sending SIGKILL after SIGTERM",
813+
)
814+
801815
parser.add_testenv_attribute(
802816
name="commands",
803817
type="argvlist",
@@ -1231,7 +1245,8 @@ def make_envconfig(self, name, section, subs, config, replace=True):
12311245
for env_attr in config._testenv_attr:
12321246
atype = env_attr.type
12331247
try:
1234-
if atype in ("bool", "path", "string", "dict", "dict_setenv", "argv", "argvlist"):
1248+
if atype in ("bool", "float", "path", "string",
1249+
"dict", "dict_setenv", "argv", "argvlist"):
12351250
meth = getattr(reader, "get{}".format(atype))
12361251
res = meth(env_attr.name, env_attr.default, replace=replace)
12371252
elif atype == "basepython":
@@ -1448,6 +1463,22 @@ def _getdict(self, value, default, sep, replace=True):
14481463

14491464
return d
14501465

1466+
def getfloat(self, name, default=None, replace=True):
1467+
s = self.getstring(name, default, replace=replace)
1468+
if not s or not replace:
1469+
s = default
1470+
if s is None:
1471+
raise KeyError("no config value [{}] {} found".format(self.section_name, name))
1472+
1473+
if not isinstance(s, float):
1474+
try:
1475+
s = float(s)
1476+
except ValueError:
1477+
raise tox.exception.ConfigError(
1478+
"{}: invalid float {!r}".format(name, s)
1479+
)
1480+
return s
1481+
14511482
def getbool(self, name, default=None, replace=True):
14521483
s = self.getstring(name, default, replace=replace)
14531484
if not s or not replace:

src/tox/session/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
from .commands.show_config import show_config
3838
from .commands.show_env import show_envs
3939

40+
WAIT_INTERRUPT = 0.3
41+
WAIT_TERMINATE = 0.2
42+
4043

4144
def cmdline(args=None):
4245
if args is None:
@@ -170,6 +173,8 @@ def newaction(self, name, msg, *args):
170173
self.resultlog.command_log,
171174
self.popen,
172175
sys.executable,
176+
WAIT_INTERRUPT,
177+
WAIT_TERMINATE,
173178
)
174179

175180
def runcommand(self):

src/tox/venv.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ def new_action(self, msg, *args):
130130
command_log,
131131
self.popen,
132132
self.envconfig.envpython,
133+
self.envconfig.interrupt_timeout,
134+
self.envconfig.terminate_timeout,
133135
)
134136

135137
def get_result_json_path(self):

0 commit comments

Comments
 (0)