Skip to content

Commit 67d4cbf

Browse files
[3.14] gh-152680: Detect container/VM in test.pythoninfo (GH-152668) (#152700)
gh-152680: Detect container/VM in test.pythoninfo (GH-152668) On Apple, log the hardware model as "system.hardware". * Log the "CI", "IMAGE_OS_VERSION" and "container" environment variables. * Add run_command() and first_line() functions. (cherry picked from commit f1c5363) Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent fd4965d commit 67d4cbf

1 file changed

Lines changed: 116 additions & 59 deletions

File tree

Lib/test/pythoninfo.py

Lines changed: 116 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99

1010

1111
MS_WINDOWS = (sys.platform == "win32")
12+
APPLE = (sys.platform in ("darwin", "ios", "tvos", "watchos"))
13+
14+
COMMAND_TIMEOUT = 60.0
1215

1316

1417
def normalize_text(text):
@@ -19,6 +22,16 @@ def normalize_text(text):
1922
return text.strip()
2023

2124

25+
def first_line(text):
26+
# Get the first line. Return text unchanged if it's empty.
27+
lines = text.splitlines()
28+
if lines:
29+
return lines[0]
30+
else:
31+
# text is an empty string
32+
return text
33+
34+
2235
def read_first_line(filename):
2336
# Get the first line of a text file and strip trailing spaces
2437
try:
@@ -293,9 +306,11 @@ def format_groups(groups):
293306
"BUILDPYTHON",
294307
"CC",
295308
"CFLAGS",
309+
"CI",
296310
"COLUMNS",
297311
"COMPUTERNAME",
298312
"COMSPEC",
313+
"CONTAINER",
299314
"CPP",
300315
"CPPFLAGS",
301316
"DISPLAY",
@@ -310,6 +325,7 @@ def format_groups(groups):
310325
"HOMEDRIVE",
311326
"HOMEPATH",
312327
"IDLESTARTUP",
328+
"IMAGE_OS_VERSION",
313329
"IPHONEOS_DEPLOYMENT_TARGET",
314330
"LANG",
315331
"LDFLAGS",
@@ -434,24 +450,47 @@ def format_attr(attr, value):
434450
info_add('readline.library', 'GNU readline')
435451

436452

437-
def collect_gdb(info_add):
453+
def run_command(cmd, check=True, **kwargs):
438454
import subprocess
455+
timeout = COMMAND_TIMEOUT
439456

457+
cmd_str = ' '.join(cmd)
440458
try:
441-
proc = subprocess.Popen(["gdb", "-nx", "--version"],
459+
proc = subprocess.Popen(cmd,
442460
stdout=subprocess.PIPE,
443-
stderr=subprocess.PIPE,
444-
universal_newlines=True)
445-
version = proc.communicate()[0]
446-
if proc.returncode:
447-
# ignore gdb failure: test_gdb will log the error
448-
return
449-
except OSError:
450-
return
461+
stderr=subprocess.DEVNULL,
462+
text=True,
463+
**kwargs)
464+
with proc:
465+
try:
466+
stdout = proc.communicate(timeout=timeout)[0]
467+
except:
468+
proc.kill()
469+
proc.communicate()
470+
raise
451471

452-
# Only keep the first line
453-
version = version.splitlines()[0]
454-
info_add('gdb_version', version)
472+
if check and proc.returncode:
473+
print(f"Command {cmd_str} failed with exit code {proc.returncode}")
474+
return ''
475+
476+
# Strip trailing spaces and newlines
477+
stdout = stdout.rstrip()
478+
return stdout
479+
except FileNotFoundError:
480+
return ''
481+
except OSError as exc:
482+
print(f"Command {cmd_str} failed with: {exc!r}")
483+
return ''
484+
except subprocess.TimeoutExpired:
485+
print(f"Command {cmd_str}: timeout!")
486+
return ''
487+
488+
489+
def collect_gdb(info_add):
490+
version = run_command(["gdb", "-nx", "--version"])
491+
if version:
492+
# Only keep the first line
493+
info_add('gdb_version', first_line(version))
455494

456495

457496
def collect_tkinter(info_add):
@@ -835,7 +874,6 @@ def collect_support_threading_helper(info_add):
835874

836875

837876
def collect_cc(info_add):
838-
import subprocess
839877
import sysconfig
840878

841879
CC = sysconfig.get_config_var('CC')
@@ -848,23 +886,17 @@ def collect_cc(info_add):
848886
except ImportError:
849887
args = CC.split()
850888
args.append('--version')
851-
try:
852-
proc = subprocess.Popen(args,
853-
stdout=subprocess.PIPE,
854-
stderr=subprocess.STDOUT,
855-
universal_newlines=True)
856-
except OSError:
889+
890+
stdout = run_command(args)
891+
if not stdout:
857892
# Cannot run the compiler, for example when Python has been
858893
# cross-compiled and installed on the target platform where the
859894
# compiler is missing.
860-
return
861-
862-
stdout = proc.communicate()[0]
863-
if proc.returncode:
895+
#
864896
# CC --version failed: ignore error
865897
return
866898

867-
text = stdout.splitlines()[0]
899+
text = first_line(stdout)
868900
text = normalize_text(text)
869901
info_add('CC.version', text)
870902

@@ -959,21 +991,11 @@ def collect_windows(info_add):
959991
pass
960992

961993
# windows.version_caption: "wmic os get Caption,Version /value" command
962-
import subprocess
963-
try:
964-
# When wmic.exe output is redirected to a pipe,
965-
# it uses the OEM code page
966-
proc = subprocess.Popen(["wmic", "os", "get", "Caption,Version", "/value"],
967-
stdout=subprocess.PIPE,
968-
stderr=subprocess.PIPE,
969-
encoding="oem",
970-
text=True)
971-
output, stderr = proc.communicate()
972-
if proc.returncode:
973-
output = ""
974-
except OSError:
975-
pass
976-
else:
994+
output = run_command(["wmic", "os", "get", "Caption,Version", "/value"],
995+
# When wmic.exe output is redirected to a pipe,
996+
# it uses the OEM code page
997+
encoding="oem")
998+
if output:
977999
for line in output.splitlines():
9781000
line = line.strip()
9791001
if line.startswith('Caption='):
@@ -986,23 +1008,11 @@ def collect_windows(info_add):
9861008
info_add('windows.version', line)
9871009

9881010
# windows.ver: "ver" command
989-
try:
990-
proc = subprocess.Popen(["ver"], shell=True,
991-
stdout=subprocess.PIPE,
992-
stderr=subprocess.PIPE,
993-
text=True)
994-
output = proc.communicate()[0]
995-
if proc.returncode == 0xc0000142:
996-
return
997-
if proc.returncode:
998-
output = ""
999-
except OSError:
1000-
return
1001-
else:
1002-
output = output.strip()
1003-
line = output.splitlines()[0]
1004-
if line:
1005-
info_add('windows.ver', line)
1011+
output = run_command(["ver"], shell=True)
1012+
# "ver" output starts with an empty line: remove it
1013+
output = output.strip()
1014+
if output:
1015+
info_add('windows.ver', first_line(output))
10061016

10071017
# windows.developer_mode: get AllowDevelopmentWithoutDevLicense registry
10081018
value = winreg_query(r"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows"
@@ -1113,7 +1123,45 @@ def get_machine_id():
11131123
return None
11141124

11151125

1116-
def collect_linux(info_add):
1126+
def detect_virt():
1127+
# Run systemd-detect-virt command
1128+
virt = run_command(["systemd-detect-virt"], check=False)
1129+
if virt and virt != "none":
1130+
return virt
1131+
1132+
# Check if the process in running in a container
1133+
import os.path
1134+
if os.path.exists('/.dockerenv'):
1135+
return 'docker'
1136+
if os.path.exists('/run/.containerenv'):
1137+
return 'podman'
1138+
1139+
container = read_first_line('/run/systemd/container')
1140+
if container:
1141+
return container
1142+
1143+
if APPLE:
1144+
hv_vmm_present = run_command(['sysctl', '-n', 'kern.hv_vmm_present'])
1145+
if hv_vmm_present == '1':
1146+
return 'run in a VM (kern.hv_vmm_present is 1)'
1147+
1148+
# Other ways to check if running in a container:
1149+
# * Parse /proc/1/mounts or /proc/1/mountinfo (check "/" filesystem).
1150+
# * Parse /proc/1/cgroup.
1151+
# * Parse the first line of /proc/1/sched (check process name is different
1152+
# than "init" and "systemd").
1153+
# * Check / inode.
1154+
# * On systems using SELinux (Fedora/CentOS/RHEL), check for "container_t"
1155+
# label, for example of /proc/1/attr/current.
1156+
# * Check for "container" variable in /proc/1/environ
1157+
# (only root can read this file).
1158+
# * Check for "container" environment variable.
1159+
# * Set a specific env var when creating the container image.
1160+
# * Run virt-what, need to install the script, and must be run as root.
1161+
# * Check for "GITHUB_ACTIONS" environmant variable (GitHub Action).
1162+
1163+
1164+
def collect_system(info_add):
11171165
boot_id = read_first_line("/proc/sys/kernel/random/boot_id")
11181166
if boot_id:
11191167
info_add('system.boot_id', boot_id)
@@ -1133,6 +1181,15 @@ def collect_linux(info_add):
11331181
uptime = f'{uptime} sec'
11341182
info_add('system.uptime', uptime)
11351183

1184+
virt = detect_virt()
1185+
if virt:
1186+
info_add('system.virt', virt)
1187+
1188+
if APPLE:
1189+
hardware = run_command(['sysctl', '-n', 'hw.model'])
1190+
if hardware:
1191+
info_add('system.hardware', hardware)
1192+
11361193

11371194
def collect_info(info):
11381195
error = False
@@ -1174,7 +1231,7 @@ def collect_info(info):
11741231
collect_windows,
11751232
collect_zlib,
11761233
collect_libregrtest_utils,
1177-
collect_linux,
1234+
collect_system,
11781235

11791236
# Collecting from tests should be last as they have side effects.
11801237
collect_test_socket,

0 commit comments

Comments
 (0)