Skip to content

Commit adaf2ff

Browse files
vstinnermiss-islington
authored andcommitted
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 2c9972c commit adaf2ff

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:
@@ -291,9 +304,11 @@ def format_groups(groups):
291304
"BUILDPYTHON",
292305
"CC",
293306
"CFLAGS",
307+
"CI",
294308
"COLUMNS",
295309
"COMPUTERNAME",
296310
"COMSPEC",
311+
"CONTAINER",
297312
"CPP",
298313
"CPPFLAGS",
299314
"DISPLAY",
@@ -308,6 +323,7 @@ def format_groups(groups):
308323
"HOMEDRIVE",
309324
"HOMEPATH",
310325
"IDLESTARTUP",
326+
"IMAGE_OS_VERSION",
311327
"IPHONEOS_DEPLOYMENT_TARGET",
312328
"LANG",
313329
"LDFLAGS",
@@ -432,24 +448,47 @@ def format_attr(attr, value):
432448
info_add('readline.library', 'GNU readline')
433449

434450

435-
def collect_gdb(info_add):
451+
def run_command(cmd, check=True, **kwargs):
436452
import subprocess
453+
timeout = COMMAND_TIMEOUT
437454

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

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

454493

455494
def collect_tkinter(info_add):
@@ -833,7 +872,6 @@ def collect_support_threading_helper(info_add):
833872

834873

835874
def collect_cc(info_add):
836-
import subprocess
837875
import sysconfig
838876

839877
CC = sysconfig.get_config_var('CC')
@@ -846,23 +884,17 @@ def collect_cc(info_add):
846884
except ImportError:
847885
args = CC.split()
848886
args.append('--version')
849-
try:
850-
proc = subprocess.Popen(args,
851-
stdout=subprocess.PIPE,
852-
stderr=subprocess.STDOUT,
853-
universal_newlines=True)
854-
except OSError:
887+
888+
stdout = run_command(args)
889+
if not stdout:
855890
# Cannot run the compiler, for example when Python has been
856891
# cross-compiled and installed on the target platform where the
857892
# compiler is missing.
858-
return
859-
860-
stdout = proc.communicate()[0]
861-
if proc.returncode:
893+
#
862894
# CC --version failed: ignore error
863895
return
864896

865-
text = stdout.splitlines()[0]
897+
text = first_line(stdout)
866898
text = normalize_text(text)
867899
info_add('CC.version', text)
868900

@@ -957,21 +989,11 @@ def collect_windows(info_add):
957989
pass
958990

959991
# windows.version_caption: "wmic os get Caption,Version /value" command
960-
import subprocess
961-
try:
962-
# When wmic.exe output is redirected to a pipe,
963-
# it uses the OEM code page
964-
proc = subprocess.Popen(["wmic", "os", "get", "Caption,Version", "/value"],
965-
stdout=subprocess.PIPE,
966-
stderr=subprocess.PIPE,
967-
encoding="oem",
968-
text=True)
969-
output, stderr = proc.communicate()
970-
if proc.returncode:
971-
output = ""
972-
except OSError:
973-
pass
974-
else:
992+
output = run_command(["wmic", "os", "get", "Caption,Version", "/value"],
993+
# When wmic.exe output is redirected to a pipe,
994+
# it uses the OEM code page
995+
encoding="oem")
996+
if output:
975997
for line in output.splitlines():
976998
line = line.strip()
977999
if line.startswith('Caption='):
@@ -984,23 +1006,11 @@ def collect_windows(info_add):
9841006
info_add('windows.version', line)
9851007

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

10051015
# windows.developer_mode: get AllowDevelopmentWithoutDevLicense registry
10061016
value = winreg_query(r"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows"
@@ -1111,7 +1121,45 @@ def get_machine_id():
11111121
return None
11121122

11131123

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

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

11351192
def collect_info(info):
11361193
error = False
@@ -1172,7 +1229,7 @@ def collect_info(info):
11721229
collect_windows,
11731230
collect_zlib,
11741231
collect_libregrtest_utils,
1175-
collect_linux,
1232+
collect_system,
11761233

11771234
# Collecting from tests should be last as they have side effects.
11781235
collect_test_socket,

0 commit comments

Comments
 (0)