Skip to content

Commit f1c5363

Browse files
authored
gh-152680: Detect container/VM in test.pythoninfo (#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.
1 parent 10ed03e commit f1c5363

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):
@@ -847,7 +886,6 @@ def collect_support_threading_helper(info_add):
847886

848887

849888
def collect_cc(info_add):
850-
import subprocess
851889
import sysconfig
852890

853891
CC = sysconfig.get_config_var('CC')
@@ -860,23 +898,17 @@ def collect_cc(info_add):
860898
except ImportError:
861899
args = CC.split()
862900
args.append('--version')
863-
try:
864-
proc = subprocess.Popen(args,
865-
stdout=subprocess.PIPE,
866-
stderr=subprocess.STDOUT,
867-
universal_newlines=True)
868-
except OSError:
901+
902+
stdout = run_command(args)
903+
if not stdout:
869904
# Cannot run the compiler, for example when Python has been
870905
# cross-compiled and installed on the target platform where the
871906
# compiler is missing.
872-
return
873-
874-
stdout = proc.communicate()[0]
875-
if proc.returncode:
907+
#
876908
# CC --version failed: ignore error
877909
return
878910

879-
text = stdout.splitlines()[0]
911+
text = first_line(stdout)
880912
text = normalize_text(text)
881913
info_add('CC.version', text)
882914

@@ -978,21 +1010,11 @@ def collect_windows(info_add):
9781010
call_func(info_add, 'windows.oem_code_page', _winapi, 'GetOEMCP')
9791011

9801012
# windows.version_caption: "wmic os get Caption,Version /value" command
981-
import subprocess
982-
try:
983-
# When wmic.exe output is redirected to a pipe,
984-
# it uses the OEM code page
985-
proc = subprocess.Popen(["wmic", "os", "get", "Caption,Version", "/value"],
986-
stdout=subprocess.PIPE,
987-
stderr=subprocess.PIPE,
988-
encoding="oem",
989-
text=True)
990-
output, stderr = proc.communicate()
991-
if proc.returncode:
992-
output = ""
993-
except OSError:
994-
pass
995-
else:
1013+
output = run_command(["wmic", "os", "get", "Caption,Version", "/value"],
1014+
# When wmic.exe output is redirected to a pipe,
1015+
# it uses the OEM code page
1016+
encoding="oem")
1017+
if output:
9961018
for line in output.splitlines():
9971019
line = line.strip()
9981020
if line.startswith('Caption='):
@@ -1005,23 +1027,11 @@ def collect_windows(info_add):
10051027
info_add('windows.version', line)
10061028

10071029
# windows.ver: "ver" command
1008-
try:
1009-
proc = subprocess.Popen(["ver"], shell=True,
1010-
stdout=subprocess.PIPE,
1011-
stderr=subprocess.PIPE,
1012-
text=True)
1013-
output = proc.communicate()[0]
1014-
if proc.returncode == 0xc0000142:
1015-
return
1016-
if proc.returncode:
1017-
output = ""
1018-
except OSError:
1019-
return
1020-
else:
1021-
output = output.strip()
1022-
line = output.splitlines()[0]
1023-
if line:
1024-
info_add('windows.ver', line)
1030+
output = run_command(["ver"], shell=True)
1031+
# "ver" output starts with an empty line: remove it
1032+
output = output.strip()
1033+
if output:
1034+
info_add('windows.ver', first_line(output))
10251035

10261036
# windows.developer_mode: get AllowDevelopmentWithoutDevLicense registry
10271037
value = winreg_query(r"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows"
@@ -1132,7 +1142,45 @@ def get_machine_id():
11321142
return None
11331143

11341144

1135-
def collect_linux(info_add):
1145+
def detect_virt():
1146+
# Run systemd-detect-virt command
1147+
virt = run_command(["systemd-detect-virt"], check=False)
1148+
if virt and virt != "none":
1149+
return virt
1150+
1151+
# Check if the process in running in a container
1152+
import os.path
1153+
if os.path.exists('/.dockerenv'):
1154+
return 'docker'
1155+
if os.path.exists('/run/.containerenv'):
1156+
return 'podman'
1157+
1158+
container = read_first_line('/run/systemd/container')
1159+
if container:
1160+
return container
1161+
1162+
if APPLE:
1163+
hv_vmm_present = run_command(['sysctl', '-n', 'kern.hv_vmm_present'])
1164+
if hv_vmm_present == '1':
1165+
return 'run in a VM (kern.hv_vmm_present is 1)'
1166+
1167+
# Other ways to check if running in a container:
1168+
# * Parse /proc/1/mounts or /proc/1/mountinfo (check "/" filesystem).
1169+
# * Parse /proc/1/cgroup.
1170+
# * Parse the first line of /proc/1/sched (check process name is different
1171+
# than "init" and "systemd").
1172+
# * Check / inode.
1173+
# * On systems using SELinux (Fedora/CentOS/RHEL), check for "container_t"
1174+
# label, for example of /proc/1/attr/current.
1175+
# * Check for "container" variable in /proc/1/environ
1176+
# (only root can read this file).
1177+
# * Check for "container" environment variable.
1178+
# * Set a specific env var when creating the container image.
1179+
# * Run virt-what, need to install the script, and must be run as root.
1180+
# * Check for "GITHUB_ACTIONS" environmant variable (GitHub Action).
1181+
1182+
1183+
def collect_system(info_add):
11361184
boot_id = read_first_line("/proc/sys/kernel/random/boot_id")
11371185
if boot_id:
11381186
info_add('system.boot_id', boot_id)
@@ -1152,6 +1200,15 @@ def collect_linux(info_add):
11521200
uptime = f'{uptime} sec'
11531201
info_add('system.uptime', uptime)
11541202

1203+
virt = detect_virt()
1204+
if virt:
1205+
info_add('system.virt', virt)
1206+
1207+
if APPLE:
1208+
hardware = run_command(['sysctl', '-n', 'hw.model'])
1209+
if hardware:
1210+
info_add('system.hardware', hardware)
1211+
11551212

11561213
def collect_info(info):
11571214
error = False
@@ -1194,7 +1251,7 @@ def collect_info(info):
11941251
collect_zlib,
11951252
collect_zstd,
11961253
collect_libregrtest_utils,
1197-
collect_linux,
1254+
collect_system,
11981255

11991256
# Collecting from tests should be last as they have side effects.
12001257
collect_test_socket,

0 commit comments

Comments
 (0)