diff --git a/docs/changelog/1772.bugfix.rst b/docs/changelog/1772.bugfix.rst new file mode 100644 index 000000000..c96ba979a --- /dev/null +++ b/docs/changelog/1772.bugfix.rst @@ -0,0 +1,2 @@ +Fix a killed tox (via SIGTERM) leaving the commands subprocesses running +by handling it as if it were a KeyboardInterrupt - by :user:`dajose` diff --git a/docs/config.rst b/docs/config.rst index f3bb1b12e..01bc9b7d6 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -607,9 +607,9 @@ Complete list of settings that you can put into ``testenv*`` sections: .. versionadded:: 3.15.2 - When an interrupt is sent via Ctrl+C, the SIGINT is sent to all foreground - processes. The :conf:``suicide_timeout`` gives the running process time to - cleanup and exit before receiving (in some cases, a duplicate) SIGINT from + When an interrupt is sent via Ctrl+C or the tox process is killed with a SIGTERM, + a SIGINT is sent to all foreground processes. The :conf:``suicide_timeout`` gives + the running process time to cleanup and exit before receiving (in some cases, a duplicate) SIGINT from tox. .. conf:: interrupt_timeout ^ float ^ 0.3 diff --git a/src/tox/action.py b/src/tox/action.py index b5381b835..e7f9b77bb 100644 --- a/src/tox/action.py +++ b/src/tox/action.py @@ -49,6 +49,10 @@ def __init__( self.suicide_timeout = suicide_timeout self.interrupt_timeout = interrupt_timeout self.terminate_timeout = terminate_timeout + if is_main_thread(): + # python allows only main thread to install signal handlers + # see https://docs.python.org/3/library/signal.html#signals-and-threads + self._install_sigterm_handler() def __enter__(self): msg = "{} {}".format(self.msg, " ".join(map(str, self.args))) @@ -278,3 +282,12 @@ def _rewrite_args(self, cwd, args): new_args.append(str(arg)) return new_args + + def _install_sigterm_handler(self): + """Handle sigterm as if it were a keyboardinterrupt""" + + def sigterm_handler(signum, frame): + reporter.error("Got SIGTERM, handling it as a KeyboardInterrupt") + raise KeyboardInterrupt() + + signal.signal(signal.SIGTERM, sigterm_handler) diff --git a/tests/integration/test_provision_int.py b/tests/integration/test_provision_int.py index 0ae411b8d..05fb1a667 100644 --- a/tests/integration/test_provision_int.py +++ b/tests/integration/test_provision_int.py @@ -73,7 +73,8 @@ def test_provision_from_pyvenv(initproj, cmd, monkeypatch): "sys.platform == 'win32'", reason="triggering SIGINT reliably on Windows is hard", ) -def test_provision_interrupt_child(initproj, monkeypatch, capfd): +@pytest.mark.parametrize("signal_type", [signal.SIGINT, signal.SIGTERM]) +def test_provision_interrupt_child(initproj, monkeypatch, capfd, signal_type): monkeypatch.delenv(str("PYTHONPATH"), raising=False) monkeypatch.setenv(str("TOX_REPORTER_TIMESTAMP"), str("1")) initproj( @@ -123,7 +124,7 @@ def test_provision_interrupt_child(initproj, monkeypatch, capfd): # 1 process for the host tox, 1 for the provisioned assert len(all_process) >= 2, all_process - process.send_signal(signal.CTRL_C_EVENT if sys.platform == "win32" else signal.SIGINT) + process.send_signal(signal.CTRL_C_EVENT if sys.platform == "win32" else signal_type) process.communicate() out, err = capfd.readouterr() assert ".tox KeyboardInterrupt: from" in out, out