mirror of
https://github.com/lutris/lutris
synced 2024-09-15 22:09:55 +00:00
Make NVIDIA checks work without /proc access
Some environments don't have access to /proc, or may only have access to specific files in /proc. We can use other methods to find the same information.
This commit is contained in:
parent
4d4d7f6319
commit
d4daf92de5
|
@ -2,10 +2,13 @@
|
|||
|
||||
Everything in this module should rely on /proc or /sys only, no executable calls
|
||||
"""
|
||||
import contextlib
|
||||
import os
|
||||
import re
|
||||
|
||||
from lutris.util.graphics.glxinfo import GlxInfo
|
||||
from lutris.util.log import logger
|
||||
from lutris.util.system import read_process_output
|
||||
|
||||
MIN_RECOMMENDED_NVIDIA_DRIVER = 415
|
||||
|
||||
|
@ -15,48 +18,90 @@ def get_nvidia_driver_info():
|
|||
version_file_path = "/proc/driver/nvidia/version"
|
||||
if not os.path.exists(version_file_path):
|
||||
return
|
||||
with open(version_file_path, encoding='utf-8') as version_file:
|
||||
content = version_file.readlines()
|
||||
nvrm_version = content[0].split(': ')[1].strip().split()
|
||||
if 'Open' in nvrm_version:
|
||||
with contextlib.suppress(OSError):
|
||||
with open(version_file_path, encoding="utf-8") as version_file:
|
||||
content = version_file.readlines()
|
||||
nvrm_version = content[0].split(": ")[1].strip().split()
|
||||
if "Open" in nvrm_version:
|
||||
return {
|
||||
'nvrm': {
|
||||
'vendor': nvrm_version[0],
|
||||
'platform': nvrm_version[1],
|
||||
'arch': nvrm_version[6],
|
||||
'version': nvrm_version[7]
|
||||
"nvrm": {
|
||||
"vendor": nvrm_version[0],
|
||||
"platform": nvrm_version[1],
|
||||
"arch": nvrm_version[6],
|
||||
"version": nvrm_version[7],
|
||||
}
|
||||
}
|
||||
return {
|
||||
'nvrm': {
|
||||
'vendor': nvrm_version[0],
|
||||
'platform': nvrm_version[1],
|
||||
'arch': nvrm_version[2],
|
||||
'version': nvrm_version[5],
|
||||
'date': ' '.join(nvrm_version[6:])
|
||||
"nvrm": {
|
||||
"vendor": nvrm_version[0],
|
||||
"platform": nvrm_version[1],
|
||||
"arch": nvrm_version[2],
|
||||
"version": nvrm_version[5],
|
||||
"date": " ".join(nvrm_version[6:]),
|
||||
}
|
||||
}
|
||||
# If the /proc file failed, look it up with glxinfo.
|
||||
glx_info = GlxInfo()
|
||||
platform = read_process_output(["uname", "-s"])
|
||||
arch = read_process_output(["uname", "-m"])
|
||||
return {
|
||||
"nvrm": {
|
||||
"vendor": glx_info.opengl_vendor,
|
||||
"platform": platform,
|
||||
"arch": arch,
|
||||
"version": glx_info.opengl_version.rsplit(maxsplit=1)[-1],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def get_nvidia_gpu_ids():
|
||||
"""Return the list of Nvidia GPUs"""
|
||||
return os.listdir("/proc/driver/nvidia/gpus")
|
||||
with contextlib.suppress(OSError):
|
||||
return os.listdir("/proc/driver/nvidia/gpus")
|
||||
values = read_process_output(
|
||||
# 10de is NVIDIA's vendor ID, 0300 gets you video controllers.
|
||||
["lspci", "-D", "-n", "-d", "10de::0300"],
|
||||
).splitlines()
|
||||
return [line.split(maxsplit=1)[0] for line in values]
|
||||
|
||||
|
||||
def get_nvidia_gpu_info(gpu_id):
|
||||
"""Return details about a GPU"""
|
||||
with open("/proc/driver/nvidia/gpus/%s/information" % gpu_id, encoding='utf-8') as info_file:
|
||||
content = info_file.readlines()
|
||||
infos = {}
|
||||
for line in content:
|
||||
with contextlib.suppress(OSError):
|
||||
with open(
|
||||
"/proc/driver/nvidia/gpus/%s/information" % gpu_id, encoding="utf-8"
|
||||
) as info_file:
|
||||
content = info_file.readlines()
|
||||
infos = {}
|
||||
for line in content:
|
||||
key, value = line.split(":", 1)
|
||||
infos[key] = value.strip()
|
||||
return infos
|
||||
lspci_data = read_process_output(["lspci", "-v", "-s", gpu_id])
|
||||
info = {
|
||||
"Model": "NVIDIA "
|
||||
+ re.search(r"NVIDIA Corporation \w+ \[(.+?)\]", lspci_data).group(1),
|
||||
"IRQ": re.search("IRQ ([0-9]+)", lspci_data).group(1),
|
||||
"Bus Location": gpu_id,
|
||||
}
|
||||
for line in lspci_data.splitlines():
|
||||
if ":" not in line:
|
||||
continue
|
||||
key, value = line.split(":", 1)
|
||||
infos[key] = value.strip()
|
||||
return infos
|
||||
info[key.strip()] = value.strip()
|
||||
return info
|
||||
|
||||
|
||||
def is_nvidia():
|
||||
"""Return true if the Nvidia drivers are currently in use"""
|
||||
return os.path.exists("/proc/driver/nvidia")
|
||||
with contextlib.suppress(OSError):
|
||||
return os.path.exists("/proc/driver/nvidia")
|
||||
with contextlib.suppress(OSError):
|
||||
with open("/proc/modules") as f:
|
||||
modules = f.read()
|
||||
return bool(re.search(r"^nvidia ", modules, flags=re.MULTILINE))
|
||||
glx_info = GlxInfo()
|
||||
return "NVIDIA" in glx_info.opengl_vendor
|
||||
|
||||
|
||||
def get_gpus():
|
||||
|
@ -67,7 +112,9 @@ def get_gpus():
|
|||
try:
|
||||
cardlist = os.listdir("/sys/class/drm/")
|
||||
except PermissionError:
|
||||
logger.error("Your system does not allow reading from /sys/class/drm, no GPU detected.")
|
||||
logger.error(
|
||||
"Your system does not allow reading from /sys/class/drm, no GPU detected."
|
||||
)
|
||||
return
|
||||
for cardname in cardlist:
|
||||
if re.match(r"^card\d$", cardname):
|
||||
|
@ -78,7 +125,9 @@ def get_gpu_info(card):
|
|||
"""Return information about a GPU"""
|
||||
infos = {"DRIVER": "", "PCI_ID": "", "PCI_SUBSYS_ID": ""}
|
||||
try:
|
||||
with open("/sys/class/drm/%s/device/uevent" % card, encoding='utf-8') as card_uevent:
|
||||
with open(
|
||||
"/sys/class/drm/%s/device/uevent" % card, encoding="utf-8"
|
||||
) as card_uevent:
|
||||
content = card_uevent.readlines()
|
||||
except FileNotFoundError:
|
||||
logger.error("Unable to read driver information for card %s", card)
|
||||
|
@ -102,14 +151,20 @@ def check_driver():
|
|||
if is_nvidia():
|
||||
driver_info = get_nvidia_driver_info()
|
||||
# pylint: disable=logging-format-interpolation
|
||||
logger.info("Using {vendor} drivers {version} for {arch}".format(**driver_info["nvrm"]))
|
||||
logger.info(
|
||||
"Using {vendor} drivers {version} for {arch}".format(**driver_info["nvrm"])
|
||||
)
|
||||
gpus = get_nvidia_gpu_ids()
|
||||
for gpu_id in gpus:
|
||||
gpu_info = get_nvidia_gpu_info(gpu_id)
|
||||
logger.info("GPU: %s", gpu_info.get("Model"))
|
||||
for card in get_gpus():
|
||||
# pylint: disable=logging-format-interpolation
|
||||
logger.info("GPU: {PCI_ID} {PCI_SUBSYS_ID} using {DRIVER} driver".format(**get_gpu_info(card)))
|
||||
logger.info(
|
||||
"GPU: {PCI_ID} {PCI_SUBSYS_ID} using {DRIVER} driver".format(
|
||||
**get_gpu_info(card)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def is_outdated():
|
||||
|
|
272
tests/unit/util/graphics/test_drivers.py
Normal file
272
tests/unit/util/graphics/test_drivers.py
Normal file
|
@ -0,0 +1,272 @@
|
|||
import io
|
||||
import subprocess
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from lutris.util.graphics import drivers, glxinfo
|
||||
|
||||
PROPRIETARY_MODULE_VERSION_FILE = """\
|
||||
NVRM version: NVIDIA UNIX x86_64 Kernel Module 525.105.17 Tue Mar 28 18:02:59 UTC 2023
|
||||
GCC version: gcc version 11.3.0 (Ubuntu 11.3.0-1ubuntu1~22.04)
|
||||
"""
|
||||
PROPRIETARY_MODULE_OUTPUT = {
|
||||
"nvrm": {
|
||||
"vendor": "NVIDIA",
|
||||
"platform": "UNIX",
|
||||
"arch": "x86_64",
|
||||
"version": "525.105.17",
|
||||
"date": "Tue Mar 28 18:02:59 UTC 2023",
|
||||
}
|
||||
}
|
||||
|
||||
OPEN_MODULE_VERSION_FILE = """\
|
||||
NVRM version: NVIDIA UNIX Open Kernel Module for x86_64 515.43.04 Release Build (archlinux-builder@archlinux)
|
||||
GCC version: gcc version 12.1.0 (GCC)
|
||||
"""
|
||||
OPEN_MODULE_OUTPUT = {
|
||||
"nvrm": {
|
||||
"vendor": "NVIDIA",
|
||||
"platform": "UNIX",
|
||||
"arch": "x86_64",
|
||||
"version": "515.43.04",
|
||||
}
|
||||
}
|
||||
DRIVER_VERSION_FILES = (
|
||||
("Proprietary", PROPRIETARY_MODULE_VERSION_FILE, PROPRIETARY_MODULE_OUTPUT),
|
||||
("Open", OPEN_MODULE_VERSION_FILE, OPEN_MODULE_OUTPUT),
|
||||
)
|
||||
SAMPLE_GLXINFO_OUTPUT = """\
|
||||
name of display: :0
|
||||
display: :0 screen: 0
|
||||
direct rendering: Yes
|
||||
Memory info (GL_NVX_gpu_memory_info):
|
||||
Dedicated video memory: 6144 MB
|
||||
Total available memory: 6144 MB
|
||||
Currently available dedicated video memory: 3359 MB
|
||||
OpenGL vendor string: NVIDIA Corporation
|
||||
OpenGL renderer string: NVIDIA GeForce GTX 1660 SUPER/PCIe/SSE2
|
||||
OpenGL core profile version string: 4.6.0 NVIDIA 525.105.17
|
||||
OpenGL core profile shading language version string: 4.60 NVIDIA
|
||||
OpenGL core profile context flags: (none)
|
||||
OpenGL core profile profile mask: core profile
|
||||
|
||||
OpenGL version string: 4.6.0 NVIDIA 525.105.17
|
||||
OpenGL shading language version string: 4.60 NVIDIA
|
||||
OpenGL context flags: (none)
|
||||
OpenGL profile mask: (none)
|
||||
|
||||
OpenGL ES profile version string: OpenGL ES 3.2 NVIDIA 525.105.17
|
||||
OpenGL ES profile shading language version string: OpenGL ES GLSL ES 3.20
|
||||
"""
|
||||
FAKE_GLXINFO_NVIDIA = glxinfo.GlxInfo(SAMPLE_GLXINFO_OUTPUT)
|
||||
SAMPLE_GPU_INFORMATION = """\
|
||||
Model: NVIDIA GeForce GTX 1660 SUPER
|
||||
IRQ: 35
|
||||
GPU UUID: GPU-12345678-1234-1234-1234-1234567890ab
|
||||
Video BIOS: 90.16.48.00.aa
|
||||
Bus Type: PCIe
|
||||
DMA Size: 47 bits
|
||||
DMA Mask: 0x7fffffffffff
|
||||
Bus Location: 0000:01:00.0
|
||||
Device Minor: 0
|
||||
GPU Excluded: No
|
||||
"""
|
||||
SAMPLE_GPU_INFO_DICT = {
|
||||
"Model": "NVIDIA GeForce GTX 1660 SUPER",
|
||||
"IRQ": "35",
|
||||
"GPU UUID": "GPU-12345678-1234-1234-1234-1234567890ab",
|
||||
"Video BIOS": "90.16.48.00.aa",
|
||||
"Bus Type": "PCIe",
|
||||
"DMA Size": "47 bits",
|
||||
"DMA Mask": "0x7fffffffffff",
|
||||
"Bus Location": "0000:01:00.0",
|
||||
"Device Minor": "0",
|
||||
"GPU Excluded": "No",
|
||||
}
|
||||
SAMPLE_LSPCI_GPU = """\
|
||||
01:00.0 VGA compatible controller: NVIDIA Corporation TU116 \
|
||||
[GeForce GTX 1660 SUPER] (rev a1) (prog-if 00 [VGA controller])
|
||||
Subsystem: eVga.com. Corp. TU116 [GeForce GTX 1660 SUPER]
|
||||
Control: I/O+ Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx+
|
||||
Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
|
||||
Latency: 0
|
||||
Interrupt: pin A routed to IRQ 35
|
||||
Region 0: Memory at f6000000 (32-bit, non-prefetchable) [size=16M]
|
||||
Region 1: Memory at e0000000 (64-bit, prefetchable) [size=256M]
|
||||
Region 3: Memory at f0000000 (64-bit, prefetchable) [size=32M]
|
||||
Region 5: I/O ports at e000 [size=128]
|
||||
Expansion ROM at 000c0000 [virtual] [disabled] [size=128K]
|
||||
Capabilities: <access denied>
|
||||
Kernel driver in use: nvidia
|
||||
Kernel modules: nvidiafb, nouveau, nvidia_drm, nvidia
|
||||
"""
|
||||
SAMPLE_PROC_MODULES = """\
|
||||
nvidia_uvm 1384448 2 - Live 0x0000000000000000 (POE)
|
||||
nvidia_drm 69632 26 - Live 0x0000000000000000 (POE)
|
||||
nvidia_modeset 1241088 67 nvidia_drm, Live 0x0000000000000000 (POE)
|
||||
uvcvideo 114688 0 - Live 0x0000000000000000
|
||||
videobuf2_vmalloc 20480 1 uvcvideo, Live 0x0000000000000000
|
||||
videobuf2_memops 20480 1 videobuf2_vmalloc, Live 0x0000000000000000
|
||||
videobuf2_v4l2 32768 1 uvcvideo, Live 0x0000000000000000
|
||||
nvidia 56500224 3776 nvidia_uvm,nvidia_modeset, Live 0x0000000000000000 (POE)
|
||||
videobuf2_common 81920 4 uvcvideo,videobuf2_vmalloc,videobuf2_memops,videobuf2_v4l2, Live 0x0000000000000000
|
||||
videodev 274432 3 uvcvideo,videobuf2_v4l2,videobuf2_common, Live 0x0000000000000000
|
||||
mc 65536 5 uvcvideo,videobuf2_v4l2,snd_usb_audio,videobuf2_common,videodev, Live 0x0000000000000000
|
||||
drm_kms_helper 200704 1 nvidia_drm, Live 0x0000000000000000
|
||||
drm 581632 30 nvidia_drm,nvidia,drm_kms_helper, Live 0x0000000000000000
|
||||
i2c_nvidia_gpu 16384 0 - Live 0x0000000000000000
|
||||
i2c_ccgx_ucsi 16384 1 i2c_nvidia_gpu, Live 0x0000000000000000
|
||||
video 65536 0 - Live 0x0000000000000000
|
||||
"""
|
||||
|
||||
|
||||
class TestGetNvidiaDriverInfo(unittest.TestCase):
|
||||
def test_success_on_current_machine(self):
|
||||
drivers.get_nvidia_driver_info()
|
||||
|
||||
@patch("os.path.exists", return_value=False)
|
||||
def test_returns_none_if_file_doesnt_exist(self, mock_path_exists):
|
||||
self.assertIsNone(drivers.get_nvidia_driver_info())
|
||||
|
||||
@patch("builtins.open")
|
||||
@patch("os.path.exists", return_value=True)
|
||||
def test_from_file(self, mock_path_exists, mock_open):
|
||||
for test_type, version_file, expected in DRIVER_VERSION_FILES:
|
||||
with self.subTest(test_type):
|
||||
mock_open.return_value = io.StringIO(version_file)
|
||||
|
||||
actual = drivers.get_nvidia_driver_info()
|
||||
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
@patch(
|
||||
"builtins.open",
|
||||
side_effect=PermissionError(
|
||||
13, "Permission Denied: '/proc/driver/nvidia/version'"
|
||||
),
|
||||
)
|
||||
@patch("os.path.exists", return_value=True)
|
||||
def test_file_errors(self, mock_path_exists, mock_open):
|
||||
with patch.object(drivers, "GlxInfo", return_value=FAKE_GLXINFO_NVIDIA), patch.object(
|
||||
subprocess,
|
||||
"run",
|
||||
side_effect=[
|
||||
subprocess.CompletedProcess([], 0, stdout="Linux\n"),
|
||||
subprocess.CompletedProcess([], 0, stdout="x86_64\n"),
|
||||
],
|
||||
):
|
||||
actual = drivers.get_nvidia_driver_info()
|
||||
|
||||
self.assertEqual(
|
||||
actual,
|
||||
{
|
||||
"nvrm": {
|
||||
"vendor": "NVIDIA Corporation",
|
||||
"platform": "Linux",
|
||||
"arch": "x86_64",
|
||||
"version": "525.105.17",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class TestGetNvidiaGpuIds(unittest.TestCase):
|
||||
|
||||
sample_gpu_list = ["0000:01:00.0"]
|
||||
|
||||
@patch("os.listdir", return_value=sample_gpu_list)
|
||||
def test_get_from_proc(self, mock_listdir):
|
||||
self.assertEqual(
|
||||
drivers.get_nvidia_gpu_ids(),
|
||||
self.sample_gpu_list
|
||||
)
|
||||
|
||||
@patch(
|
||||
"subprocess.run",
|
||||
return_value=subprocess.CompletedProcess(
|
||||
args=["lspci", "-D", "-n", "-d", "10de::0300"],
|
||||
returncode=0,
|
||||
stdout="0000:01:00.0 0300: 10de:21c4 (rev a1)\n"
|
||||
)
|
||||
)
|
||||
@patch("os.listdir", side_effect=PermissionError())
|
||||
def test_get_from_lspci(self, mock_listdir, mock_lspci):
|
||||
self.assertEqual(
|
||||
drivers.get_nvidia_gpu_ids(),
|
||||
self.sample_gpu_list
|
||||
)
|
||||
|
||||
|
||||
class TestGetNvidiaGpuInfo(unittest.TestCase):
|
||||
|
||||
sample_gpu_id = "0000:01:00.0"
|
||||
|
||||
@patch("builtins.open", return_value=io.StringIO(SAMPLE_GPU_INFORMATION))
|
||||
def test_get_from_proc(self, mock_open):
|
||||
result = drivers.get_nvidia_gpu_info(self.sample_gpu_id)
|
||||
|
||||
self.assertEqual(result, SAMPLE_GPU_INFO_DICT)
|
||||
|
||||
@patch(
|
||||
"subprocess.run",
|
||||
return_value=subprocess.CompletedProcess(
|
||||
[], 0,
|
||||
stdout=SAMPLE_LSPCI_GPU
|
||||
)
|
||||
)
|
||||
@patch("builtins.open", side_effect=PermissionError())
|
||||
def test_get_from_lspci_glxinfo(self, mock_open, mock_lspci):
|
||||
result = drivers.get_nvidia_gpu_info(self.sample_gpu_id)
|
||||
|
||||
self.assertDictContainsSubset(
|
||||
{
|
||||
"Model": "NVIDIA GeForce GTX 1660 SUPER",
|
||||
"IRQ": "35",
|
||||
"Bus Location": self.sample_gpu_id,
|
||||
"Subsystem": "eVga.com. Corp. TU116 [GeForce GTX 1660 SUPER]",
|
||||
"Interrupt": "pin A routed to IRQ 35",
|
||||
"Region 0": "Memory at f6000000 (32-bit, non-prefetchable) [size=16M]",
|
||||
"Region 1": "Memory at e0000000 (64-bit, prefetchable) [size=256M]",
|
||||
"Region 3": "Memory at f0000000 (64-bit, prefetchable) [size=32M]",
|
||||
"Region 5": "I/O ports at e000 [size=128]",
|
||||
"Kernel driver in use": "nvidia",
|
||||
},
|
||||
result
|
||||
)
|
||||
|
||||
|
||||
class TestIsNvidia(unittest.TestCase):
|
||||
|
||||
def test_success_on_current_machine(self):
|
||||
self.assertIsInstance(drivers.is_nvidia(), bool)
|
||||
|
||||
@patch("os.path.exists", return_value=False)
|
||||
def test_not_nvidia_by_directory(self, mock_exists):
|
||||
self.assertFalse(drivers.is_nvidia())
|
||||
|
||||
@patch("builtins.open", return_value=io.StringIO(""))
|
||||
@patch("os.path.exists", side_effect=PermissionError())
|
||||
def test_not_nvidia_proc_modules(self, mock_exists, mock_open):
|
||||
self.assertFalse(drivers.is_nvidia())
|
||||
|
||||
# TODO: Add AMD GLX info and uncomment this test.
|
||||
# @patch.object(drivers, "GlxInfo", return_value=FAKE_GLXINFO_AMD)
|
||||
# @patch("builtins.open", side_effect=PermissionError())
|
||||
# @patch("os.path.exists", side_effect=PermissionError())
|
||||
# def test_not_nvidia_glxinfo(self, mock_exists, mock_open, mock_glxinfo):
|
||||
# self.assertFalse(drivers.is_nvidia())
|
||||
|
||||
@patch("os.path.exists", return_value=True)
|
||||
def test_is_nvidia_by_directory(self, mock_exists):
|
||||
self.assertTrue(drivers.is_nvidia())
|
||||
|
||||
@patch("builtins.open", return_value=io.StringIO(SAMPLE_PROC_MODULES))
|
||||
@patch("os.path.exists", side_effect=PermissionError())
|
||||
def test_is_nvidia_proc_modules(self, mock_exists, mock_open):
|
||||
self.assertTrue(drivers.is_nvidia())
|
||||
|
||||
@patch.object(drivers, "GlxInfo", return_value=FAKE_GLXINFO_NVIDIA)
|
||||
@patch("builtins.open", side_effect=PermissionError())
|
||||
@patch("os.path.exists", side_effect=PermissionError())
|
||||
def test_is_nvidia_glxinfo(self, mock_exists, mock_open, mock_glxinfo):
|
||||
self.assertTrue(drivers.is_nvidia())
|
Loading…
Reference in a new issue