]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-109566: regrtest _add_python_opts() handles KeyboardInterrupt (#110062)
authorVictor Stinner <vstinner@python.org>
Fri, 29 Sep 2023 00:51:22 +0000 (02:51 +0200)
committerGitHub <noreply@github.com>
Fri, 29 Sep 2023 00:51:22 +0000 (00:51 +0000)
In the subprocess code path, wait until the child process completes
with a timeout of EXIT_TIMEOUT seconds.

Fix create_worker_process() regression: use start_new_session=True if
USE_PROCESS_GROUP is true.

WorkerThread.wait_stopped() uses a timeout of 60 seconds, instead of
30 seconds.

Lib/test/libregrtest/main.py
Lib/test/libregrtest/run_workers.py
Lib/test/libregrtest/utils.py
Lib/test/libregrtest/worker.py

index 45a68a8465d8e0ebcb0c6e770dca1164e6e43d06..dcb2c5870de176fcb8249f85b00ee5f658e32853 100644 (file)
@@ -11,18 +11,18 @@ from test.support import os_helper
 from .cmdline import _parse_args, Namespace
 from .findtests import findtests, split_test_packages, list_cases
 from .logger import Logger
+from .pgo import setup_pgo_tests
 from .result import State
+from .results import TestResults, EXITCODE_INTERRUPTED
 from .runtests import RunTests, HuntRefleak
 from .setup import setup_process, setup_test_dir
 from .single import run_single_test, PROGRESS_MIN_TIME
-from .pgo import setup_pgo_tests
-from .results import TestResults
 from .utils import (
     StrPath, StrJSON, TestName, TestList, TestTuple, FilterTuple,
     strip_py_suffix, count, format_duration,
     printlist, get_temp_dir, get_work_dir, exit_timeout,
     display_header, cleanup_temp_dir, print_warning,
-    MS_WINDOWS)
+    MS_WINDOWS, EXIT_TIMEOUT)
 
 
 class Regrtest:
@@ -525,10 +525,23 @@ class Regrtest:
         try:
             if hasattr(os, 'execv') and not MS_WINDOWS:
                 os.execv(cmd[0], cmd)
-                # execv() do no return and so we don't get to this line on success
+                # On success, execv() do no return.
+                # On error, it raises an OSError.
             else:
                 import subprocess
-                proc = subprocess.run(cmd)
+                with subprocess.Popen(cmd) as proc:
+                    try:
+                        proc.wait()
+                    except KeyboardInterrupt:
+                        # There is no need to call proc.terminate(): on CTRL+C,
+                        # SIGTERM is also sent to the child process.
+                        try:
+                            proc.wait(timeout=EXIT_TIMEOUT)
+                        except subprocess.TimeoutExpired:
+                            proc.kill()
+                            proc.wait()
+                            sys.exit(EXITCODE_INTERRUPTED)
+
                 sys.exit(proc.returncode)
         except Exception as exc:
             print_warning(f"Failed to change Python options: {exc!r}\n"
index 89cc50b7c158d278f55689ca6e534e717fa421fa..41ed7b0bac01ad5652fd305c556c0429aad61155 100644 (file)
@@ -42,7 +42,10 @@ MAIN_PROCESS_TIMEOUT = 5 * 60.0
 assert MAIN_PROCESS_TIMEOUT >= PROGRESS_UPDATE
 
 # Time to wait until a worker completes: should be immediate
-JOIN_TIMEOUT = 30.0   # seconds
+WAIT_COMPLETED_TIMEOUT = 30.0   # seconds
+
+# Time to wait a killed process (in seconds)
+WAIT_KILLED_TIMEOUT = 60.0
 
 
 # We do not use a generator so multiple threads can call next().
@@ -138,7 +141,7 @@ class WorkerThread(threading.Thread):
         if USE_PROCESS_GROUP:
             what = f"{self} process group"
         else:
-            what = f"{self}"
+            what = f"{self} process"
 
         print(f"Kill {what}", file=sys.stderr, flush=True)
         try:
@@ -390,10 +393,10 @@ class WorkerThread(threading.Thread):
         popen = self._popen
 
         try:
-            popen.wait(JOIN_TIMEOUT)
+            popen.wait(WAIT_COMPLETED_TIMEOUT)
         except (subprocess.TimeoutExpired, OSError) as exc:
             print_warning(f"Failed to wait for {self} completion "
-                          f"(timeout={format_duration(JOIN_TIMEOUT)}): "
+                          f"(timeout={format_duration(WAIT_COMPLETED_TIMEOUT)}): "
                           f"{exc!r}")
 
     def wait_stopped(self, start_time: float) -> None:
@@ -414,7 +417,7 @@ class WorkerThread(threading.Thread):
                 break
             dt = time.monotonic() - start_time
             self.log(f"Waiting for {self} thread for {format_duration(dt)}")
-            if dt > JOIN_TIMEOUT:
+            if dt > WAIT_KILLED_TIMEOUT:
                 print_warning(f"Failed to join {self} in {format_duration(dt)}")
                 break
 
index bedf9a5420db64c6db9b75cbba96541ad166cfe5..46451152b8859f813f673e853dce954edbdfcc3e 100644 (file)
@@ -541,7 +541,7 @@ def display_header(use_resources: tuple[str, ...]):
         print(f"== resources ({len(use_resources)}): "
               f"{', '.join(sorted(use_resources))}")
     else:
-        print(f"== resources: (all disabled, use -u option)")
+        print("== resources: (all disabled, use -u option)")
 
     # This makes it easier to remember what to set in your local
     # environment when trying to reproduce a sanitizer failure.
index 67f26cfe75fbe4dfd5d25109446a910a2f926663..a9c8be0bb65d087fa7085f721d107202136970eb 100644 (file)
@@ -41,14 +41,15 @@ def create_worker_process(runtests: RunTests, output_fd: int,
         env['TEMP'] = tmp_dir
         env['TMP'] = tmp_dir
 
+    # Running the child from the same working directory as regrtest's original
+    # invocation ensures that TEMPDIR for the child is the same when
+    # sysconfig.is_python_build() is true. See issue 15300.
+    #
     # Emscripten and WASI Python must start in the Python source code directory
     # to get 'python.js' or 'python.wasm' file. Then worker_process() changes
     # to a temporary directory created to run tests.
     work_dir = os_helper.SAVEDCWD
 
-    # Running the child from the same working directory as regrtest's original
-    # invocation ensures that TEMPDIR for the child is the same when
-    # sysconfig.is_python_build() is true. See issue 15300.
     kwargs: dict[str, Any] = dict(
         env=env,
         stdout=output_fd,
@@ -58,6 +59,8 @@ def create_worker_process(runtests: RunTests, output_fd: int,
         close_fds=True,
         cwd=work_dir,
     )
+    if USE_PROCESS_GROUP:
+        kwargs['start_new_session'] = True
 
     # Pass json_file to the worker process
     json_file = runtests.json_file