From a39df7fd79e1b0b3959ab5a14ac736bb0a69d594 Mon Sep 17 00:00:00 2001 From: Peng Zhang Date: Mon, 20 Jul 2026 13:47:14 -0700 Subject: [PATCH] [Python] Avoid SIGTTOU while preloading readline --- python/tvm/base.py | 32 ++++++- .../test_runtime_import.py | 95 +++++++++++++++++++ 2 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 tests/python/all-platform-minimal-test/test_runtime_import.py diff --git a/python/tvm/base.py b/python/tvm/base.py index e53f0a21dfbf..267a64881d2a 100644 --- a/python/tvm/base.py +++ b/python/tvm/base.py @@ -66,11 +66,33 @@ _RUNTIME_ONLY = True -try: - # The following import is needed for TVM to work with pdb - import readline # pylint: disable=unused-import -except ImportError: - pass +def _is_background_process_group(): + """Return whether this process group is background on its controlling TTY.""" + if not hasattr(os, "tcgetpgrp"): + return False + + try: + tty_fd = os.open("/dev/tty", os.O_RDONLY | os.O_NOCTTY) + except OSError: + return False + + try: + return os.tcgetpgrp(tty_fd) != os.getpgrp() + except OSError: + return False + finally: + os.close(tty_fd) + + +# Import readline for pdb compatibility. A libedit-backed readline touches the +# terminal during import, so skip it while this process is in a background +# process group. +if not _is_background_process_group(): + try: + import readline # pylint: disable=unused-import + except ImportError: + pass + # version number __version__ = libinfo.__version__ diff --git a/tests/python/all-platform-minimal-test/test_runtime_import.py b/tests/python/all-platform-minimal-test/test_runtime_import.py new file mode 100644 index 000000000000..82771df8119d --- /dev/null +++ b/tests/python/all-platform-minimal-test/test_runtime_import.py @@ -0,0 +1,95 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""TVM import behavior tests.""" + +import errno +import os +import sys + +import pytest + +pytestmark = pytest.mark.skipif(os.name != "posix", reason="requires POSIX job control") + + +def _run_in_pty(script): + import pty + + pid, output_fd = pty.fork() + if pid == 0: + os.execl(sys.executable, sys.executable, "-c", script) + + output = bytearray() + while True: + try: + chunk = os.read(output_fd, 4096) + except OSError as err: + if err.errno == errno.EIO: + break + raise + if not chunk: + break + output.extend(chunk) + + _, status = os.waitpid(pid, 0) + os.close(output_fd) + assert os.waitstatus_to_exitcode(status) == 0, output.decode(errors="replace") + + +def test_import_preloads_readline_for_foreground_tty(): + _run_in_pty( + """ +import importlib.util +import sys + +assert "readline" not in sys.modules +readline_is_available = importlib.util.find_spec("readline") is not None +import tvm +assert ("readline" in sys.modules) == readline_is_available +import pdb +pdb.Pdb() +""" + ) + + +def test_import_does_not_stop_background_process_group(): + _run_in_pty( + """ +import os +import signal +import sys + +for redirect_stdin in (False, True): + pid = os.fork() + if pid == 0: + os.setpgid(0, 0) + if redirect_stdin: + stdin_fd = os.open(os.devnull, os.O_RDONLY) + os.dup2(stdin_fd, 0) + os.close(stdin_fd) + import tvm + assert "readline" not in sys.modules + os._exit(0) + + _, status = os.waitpid(pid, os.WUNTRACED) + if os.WIFSTOPPED(status): + os.kill(pid, signal.SIGKILL) + os.waitpid(pid, 0) + raise AssertionError(f"TVM import stopped by signal {os.WSTOPSIG(status)}") + if os.waitstatus_to_exitcode(status) != 0: + raise AssertionError(f"TVM import exited with status {status}") +""" + )