From 8b40dff3fe8a81986b19b0968811caf9ac986af1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 30 Jun 2026 12:10:41 +0200 Subject: [PATCH 1/9] gh-152680: Check if running in a container/VM in test.pythoninfo Log also the "CI" and "container" environment variable. --- Lib/test/pythoninfo.py | 138 +++++++++++++++++++++++++---------------- 1 file changed, 86 insertions(+), 52 deletions(-) diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index a2767f3975bed25..2b49319a5ab98e5 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -9,6 +9,7 @@ MS_WINDOWS = (sys.platform == "win32") +COMMAND_TIMEOUT = 60.0 def normalize_text(text): @@ -293,6 +294,7 @@ def format_groups(groups): "BUILDPYTHON", "CC", "CFLAGS", + "CI", "COLUMNS", "COMPUTERNAME", "COMSPEC", @@ -355,6 +357,9 @@ def format_groups(groups): "_PYTHON_SYSCONFIGDATA_PATH", "__PYVENV_LAUNCHER__", + # Lower case variables + "container", + # Sanitizer options "ASAN_OPTIONS", "LSAN_OPTIONS", @@ -434,19 +439,40 @@ def format_attr(attr, value): info_add('readline.library', 'GNU readline') -def collect_gdb(info_add): +def run_command(cmd, **kwargs): import subprocess + timeout = COMMAND_TIMEOUT try: - proc = subprocess.Popen(["gdb", "-nx", "--version"], + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True) - version = proc.communicate()[0] + stderr=subprocess.DEVNULL, + text=True, + **kwargs) + # ignore stderr + with proc: + try: + stdout = proc.communicate(timeout=timeout)[0] + except: + proc.kill() + proc.communicate() + raise + if proc.returncode: - # ignore gdb failure: test_gdb will log the error - return + return '' + + # Strip trailing spaces and newlines + return stdout.rstrip() except OSError: + return '' + except subprocess.TimeoutExpired: + print(f"ERROR: Command {' '.join(cmd)}: timeout!") + return '' + + +def collect_gdb(info_add): + version = run_command(["gdb", "-nx", "--version"]) + if not version: return # Only keep the first line @@ -847,7 +873,6 @@ def collect_support_threading_helper(info_add): def collect_cc(info_add): - import subprocess import sysconfig CC = sysconfig.get_config_var('CC') @@ -860,19 +885,13 @@ def collect_cc(info_add): except ImportError: args = CC.split() args.append('--version') - try: - proc = subprocess.Popen(args, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - universal_newlines=True) - except OSError: + + stdout = run_command(args) + if not stdout: # Cannot run the compiler, for example when Python has been # cross-compiled and installed on the target platform where the # compiler is missing. - return - - stdout = proc.communicate()[0] - if proc.returncode: + # # CC --version failed: ignore error return @@ -978,21 +997,11 @@ def collect_windows(info_add): call_func(info_add, 'windows.oem_code_page', _winapi, 'GetOEMCP') # windows.version_caption: "wmic os get Caption,Version /value" command - import subprocess - try: - # When wmic.exe output is redirected to a pipe, - # it uses the OEM code page - proc = subprocess.Popen(["wmic", "os", "get", "Caption,Version", "/value"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - encoding="oem", - text=True) - output, stderr = proc.communicate() - if proc.returncode: - output = "" - except OSError: - pass - else: + output = run_command(["wmic", "os", "get", "Caption,Version", "/value"], + # When wmic.exe output is redirected to a pipe, + # it uses the OEM code page + encoding="oem") + if output: for line in output.splitlines(): line = line.strip() if line.startswith('Caption='): @@ -1005,23 +1014,11 @@ def collect_windows(info_add): info_add('windows.version', line) # windows.ver: "ver" command - try: - proc = subprocess.Popen(["ver"], shell=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True) - output = proc.communicate()[0] - if proc.returncode == 0xc0000142: - return - if proc.returncode: - output = "" - except OSError: - return - else: - output = output.strip() - line = output.splitlines()[0] - if line: - info_add('windows.ver', line) + output = run_command(["ver"], shell=True) + if output: + first_line = output.splitlines()[0] + if first_line: + info_add('windows.ver', first_line) # windows.developer_mode: get AllowDevelopmentWithoutDevLicense registry value = winreg_query(r"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows" @@ -1132,7 +1129,40 @@ def get_machine_id(): return None -def collect_linux(info_add): +def detect_virt(): + # Run systemd-detect-virt command + virt = run_command(["systemd-detect-virt"]) + if virt and virt != "none": + return virt + + # Check if the process in running in a container + import os.path + if os.path.exists('/.dockerenv'): + return 'docker' + if os.path.exists('/run/.containerenv'): + return 'podman' + + container = read_first_line('/run/systemd/container') + if container: + return container + + # Other ways to check if running in a container: + # * Parse /proc/1/mounts or /proc/1/mountinfo (check "/" filesystem). + # * Parse /proc/1/cgroup. + # * Parse the first line of /proc/1/sched (check process name is different + # than "init" and "systemd"). + # * Check / inode. + # * On systems using SELinux (Fedora/CentOS/RHEL), check for "container_t" + # label, for example of /proc/1/attr/current. + # * Check for "container" variable in /proc/1/environ + # (only root can read this file). + # * Check for "container" environment variable. + # * Set a specific env var when creating the container image. + # * Run virt-what, need to install the script, and must be run as root. + # * Check for "GITHUB_ACTIONS" environmant variable (GitHub Action). + + +def collect_system(info_add): boot_id = read_first_line("/proc/sys/kernel/random/boot_id") if boot_id: info_add('system.boot_id', boot_id) @@ -1152,6 +1182,10 @@ def collect_linux(info_add): uptime = f'{uptime} sec' info_add('system.uptime', uptime) + virt = detect_virt() + if virt: + info_add('system.virt', virt) + def collect_info(info): error = False @@ -1194,7 +1228,7 @@ def collect_info(info): collect_zlib, collect_zstd, collect_libregrtest_utils, - collect_linux, + collect_system, # Collecting from tests should be last as they have side effects. collect_test_socket, From fa61ac8dc80f2b70336bceef1443117ef7d878ca Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 30 Jun 2026 14:20:05 +0200 Subject: [PATCH 2/9] Log run_command() errors --- Lib/test/pythoninfo.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index 2b49319a5ab98e5..2aff5f06d7cb2de 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -439,10 +439,11 @@ def format_attr(attr, value): info_add('readline.library', 'GNU readline') -def run_command(cmd, **kwargs): +def run_command(cmd, check=True, **kwargs): import subprocess timeout = COMMAND_TIMEOUT + cmd_str = ' '.join(cmd) try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, @@ -458,15 +459,19 @@ def run_command(cmd, **kwargs): proc.communicate() raise - if proc.returncode: + if check and proc.returncode: + print(f"Command {cmd_str} failed with exit code {proc.returncode}") return '' # Strip trailing spaces and newlines return stdout.rstrip() - except OSError: + except FileNotFoundError: + return '' + except OSError as exc: + print(f"Command {cmd_str} failed with: {exc!r}") return '' except subprocess.TimeoutExpired: - print(f"ERROR: Command {' '.join(cmd)}: timeout!") + print(f"Command {cmd_str}: timeout!") return '' @@ -1131,7 +1136,7 @@ def get_machine_id(): def detect_virt(): # Run systemd-detect-virt command - virt = run_command(["systemd-detect-virt"]) + virt = run_command(["systemd-detect-virt"], check=False) if virt and virt != "none": return virt From 54909261aa3fab56193f65d0bcd2cf19123641bc Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 30 Jun 2026 14:59:06 +0200 Subject: [PATCH 3/9] DEBUG: log run_command() output --- Lib/test/pythoninfo.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index 2aff5f06d7cb2de..191cece6f4b733a 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -464,8 +464,11 @@ def run_command(cmd, check=True, **kwargs): return '' # Strip trailing spaces and newlines - return stdout.rstrip() + print(f"Command {cmd_str} succeeded: {stdout!r}") + stdout = stdout.rstrip() + return stdout except FileNotFoundError: + print(f"Command {cmd_str} failed with: {exc!r}") return '' except OSError as exc: print(f"Command {cmd_str} failed with: {exc!r}") From ebe242968a32b7302b21cb500fdcb251e78a8e3f Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 30 Jun 2026 15:04:59 +0200 Subject: [PATCH 4/9] DEBUG; log "sysctl -n hw.model" on Apple --- Lib/test/pythoninfo.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index 191cece6f4b733a..b669d296a841fb0 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -9,6 +9,8 @@ MS_WINDOWS = (sys.platform == "win32") +APPLE = (sys.platform in ("darwin", "ios", "tvos", "watchos")) + COMMAND_TIMEOUT = 60.0 @@ -464,11 +466,11 @@ def run_command(cmd, check=True, **kwargs): return '' # Strip trailing spaces and newlines - print(f"Command {cmd_str} succeeded: {stdout!r}") + print(f"DEBUG: Command {cmd_str} succeeded: {stdout!r}") stdout = stdout.rstrip() return stdout - except FileNotFoundError: - print(f"Command {cmd_str} failed with: {exc!r}") + except FileNotFoundError as exc: + print(f"DEBUG: Command {cmd_str} failed with: {exc!r}") return '' except OSError as exc: print(f"Command {cmd_str} failed with: {exc!r}") @@ -1194,6 +1196,11 @@ def collect_system(info_add): if virt: info_add('system.virt', virt) + if APPLE: + hardware = run_command(['sysctl', '-n', 'hw.model') + if hardware: + info_add('system.hardware', hardware) + def collect_info(info): error = False From bf2f0580f257023066cb0338209c4a6fc2f64109 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 30 Jun 2026 15:09:37 +0200 Subject: [PATCH 5/9] log kern.hv_vmm_present --- Lib/test/pythoninfo.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index b669d296a841fb0..c6d248ffdbb5b38 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -1156,6 +1156,11 @@ def detect_virt(): if container: return container + if APPLE: + hv_vmm_present = run_command(['sysctl', 'kern.hv_vmm_present']) + if hv_vmm_present == '1': + return 'run in a VM (kern.hv_vmm_present is 1)' + # Other ways to check if running in a container: # * Parse /proc/1/mounts or /proc/1/mountinfo (check "/" filesystem). # * Parse /proc/1/cgroup. @@ -1197,7 +1202,7 @@ def collect_system(info_add): info_add('system.virt', virt) if APPLE: - hardware = run_command(['sysctl', '-n', 'hw.model') + hardware = run_command(['sysctl', '-n', 'hw.model']) if hardware: info_add('system.hardware', hardware) From f60c855384803f511fc3c59a15b70e25d74a4b79 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 30 Jun 2026 15:16:32 +0200 Subject: [PATCH 6/9] More bugfixes * Fix ver command * Fix kern.hv_vmm_present test * Log also IMAGE_OS_VERSION env var. * Remove debug messages --- Lib/test/pythoninfo.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index c6d248ffdbb5b38..d621831c02b73e5 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -314,6 +314,7 @@ def format_groups(groups): "HOMEDRIVE", "HOMEPATH", "IDLESTARTUP", + "IMAGE_OS_VERSION", "IPHONEOS_DEPLOYMENT_TARGET", "LANG", "LDFLAGS", @@ -466,11 +467,9 @@ def run_command(cmd, check=True, **kwargs): return '' # Strip trailing spaces and newlines - print(f"DEBUG: Command {cmd_str} succeeded: {stdout!r}") stdout = stdout.rstrip() return stdout - except FileNotFoundError as exc: - print(f"DEBUG: Command {cmd_str} failed with: {exc!r}") + except FileNotFoundError: return '' except OSError as exc: print(f"Command {cmd_str} failed with: {exc!r}") @@ -1025,6 +1024,8 @@ def collect_windows(info_add): # windows.ver: "ver" command output = run_command(["ver"], shell=True) + # "ver" output starts with an empty line: remove it + output = output.strip() if output: first_line = output.splitlines()[0] if first_line: @@ -1157,7 +1158,7 @@ def detect_virt(): return container if APPLE: - hv_vmm_present = run_command(['sysctl', 'kern.hv_vmm_present']) + hv_vmm_present = run_command(['sysctl', '-n', 'kern.hv_vmm_present']) if hv_vmm_present == '1': return 'run in a VM (kern.hv_vmm_present is 1)' From 54b964b5b9ce2834c78c33b5dc90e9ced53d1bf6 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 30 Jun 2026 15:49:58 +0200 Subject: [PATCH 7/9] Add first_line() function --- Lib/test/pythoninfo.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index d621831c02b73e5..4fb1ff706d856cf 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -22,6 +22,16 @@ def normalize_text(text): return text.strip() +def first_line(text): + # Get the first line. Return text unchanged if it's empty. + lines = text.splitlines() + if lines: + return lines[0] + else: + # text is an empty string + return text + + def read_first_line(filename): # Get the first line of a text file and strip trailing spaces try: @@ -481,12 +491,9 @@ def run_command(cmd, check=True, **kwargs): def collect_gdb(info_add): version = run_command(["gdb", "-nx", "--version"]) - if not version: - return - - # Only keep the first line - version = version.splitlines()[0] - info_add('gdb_version', version) + if version: + # Only keep the first line + info_add('gdb_version', first_line(version)) def collect_tkinter(info_add): @@ -904,7 +911,7 @@ def collect_cc(info_add): # CC --version failed: ignore error return - text = stdout.splitlines()[0] + text = first_line(stdout) text = normalize_text(text) info_add('CC.version', text) @@ -1027,9 +1034,7 @@ def collect_windows(info_add): # "ver" output starts with an empty line: remove it output = output.strip() if output: - first_line = output.splitlines()[0] - if first_line: - info_add('windows.ver', first_line) + info_add('windows.ver', first_line(output)) # windows.developer_mode: get AllowDevelopmentWithoutDevLicense registry value = winreg_query(r"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows" From a49567ec8c2c023e4f32eb27c33449592a260047 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 30 Jun 2026 15:53:29 +0200 Subject: [PATCH 8/9] Remove outdated comment --- Lib/test/pythoninfo.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index 4fb1ff706d856cf..2c947a7edd6a5d3 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -463,7 +463,6 @@ def run_command(cmd, check=True, **kwargs): stderr=subprocess.DEVNULL, text=True, **kwargs) - # ignore stderr with proc: try: stdout = proc.communicate(timeout=timeout)[0] From 64a5ced46348573c79d07894ce8840c569aba979 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 30 Jun 2026 16:43:42 +0200 Subject: [PATCH 9/9] Fix os.environ[container] --- Lib/test/pythoninfo.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/test/pythoninfo.py b/Lib/test/pythoninfo.py index 2c947a7edd6a5d3..d7fd98a8d4311d4 100644 --- a/Lib/test/pythoninfo.py +++ b/Lib/test/pythoninfo.py @@ -310,6 +310,7 @@ def format_groups(groups): "COLUMNS", "COMPUTERNAME", "COMSPEC", + "CONTAINER", "CPP", "CPPFLAGS", "DISPLAY", @@ -370,9 +371,6 @@ def format_groups(groups): "_PYTHON_SYSCONFIGDATA_PATH", "__PYVENV_LAUNCHER__", - # Lower case variables - "container", - # Sanitizer options "ASAN_OPTIONS", "LSAN_OPTIONS",