Merge branch 'develop' of https://github.com/nicolargo/glances into develop

This commit is contained in:
Drakarah 2025-12-22 07:27:41 +00:00
commit 507079491a
28 changed files with 1054 additions and 832 deletions

View File

@ -37,7 +37,9 @@ jobs:
runs-on: ubuntu-24.04
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
# Python EOL version are note tested
# Multiple Python version only tested for Linux
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:

View File

@ -299,12 +299,12 @@ Following shells are supported: bash, zsh and tcsh.
Requirements 🧩
===============
Glances is developed in Python. A minimal Python version 3.9 or higher
Glances is developed in Python. A minimal Python version 3.10 or higher
should be installed on your system.
*Note for Python 2 users*
Glances version 4 or higher do not support Python 2 (and Python 3 < 3.9).
Glances version 4 or higher do not support Python 2 (and Python 3 < 3.10).
Please uses Glances version 3.4.x if you need Python 2 support.
Dependencies:

View File

@ -540,12 +540,12 @@ Following shells are supported: bash, zsh and tcsh.
Requirements 🧩
===============
Glances is developed in Python. A minimal Python version 3.9 or higher
Glances is developed in Python. A minimal Python version 3.10 or higher
should be installed on your system.
*Note for Python 2 users*
Glances version 4 or higher do not support Python 2 (and Python 3 < 3.9).
Glances version 4 or higher do not support Python 2 (and Python 3 < 3.10).
Please uses Glances version 3.4.x if you need Python 2 support.
Dependencies:

View File

@ -56,7 +56,7 @@ elasticsearch==9.2.0 ; python_full_version >= '3.10'
# via glances
exceptiongroup==1.2.2 ; python_full_version < '3.11'
# via anyio
fastapi==0.124.0
fastapi==0.125.0
# via glances
geomet==1.1.0
# via cassandra-driver
@ -100,7 +100,7 @@ msgpack==1.1.2
# via influxdb
netifaces2==0.0.22
# via glances
nvidia-ml-py==13.580.82
nvidia-ml-py==13.590.44
# via glances
packaging==25.0
# via glances
@ -126,11 +126,11 @@ psutil==7.1.3
# via glances
psycopg==3.2.13 ; python_full_version < '3.10'
# via glances
psycopg==3.3.1 ; python_full_version >= '3.10'
psycopg==3.3.2 ; python_full_version >= '3.10'
# via glances
psycopg-binary==3.2.13 ; python_full_version < '3.10' and implementation_name != 'pypy'
# via psycopg
psycopg-binary==3.3.1 ; python_full_version >= '3.10' and implementation_name != 'pypy'
psycopg-binary==3.3.2 ; python_full_version >= '3.10' and implementation_name != 'pypy'
# via psycopg
pyarrow==21.0.0 ; python_full_version < '3.10'
# via influxdb3-python
@ -144,7 +144,7 @@ pydantic==2.12.5
# via fastapi
pydantic-core==2.41.5
# via pydantic
pygal==3.0.5
pygal==3.1.0
# via glances
pyjwt==2.10.1
# via
@ -236,9 +236,9 @@ typing-extensions==4.15.0
# uvicorn
typing-inspection==0.4.2
# via pydantic
tzdata==2025.2 ; sys_platform == 'win32'
tzdata==2025.3 ; sys_platform == 'win32'
# via psycopg
urllib3==2.6.0
urllib3==2.6.2
# via
# docker
# elastic-transport

View File

@ -42,8 +42,10 @@ certifi==2025.11.12
# httpx
# requests
# selenium
cffi==2.0.0 ; implementation_name != 'pypy' and os_name == 'nt'
# via trio
cffi==2.0.0 ; (python_full_version >= '3.10' and implementation_name == 'pypy' and platform_python_implementation != 'PyPy') or (python_full_version >= '3.10' and os_name != 'nt' and platform_python_implementation != 'PyPy') or (implementation_name != 'pypy' and os_name == 'nt')
# via
# cryptography
# trio
cfgv==3.4.0 ; python_full_version < '3.10'
# via pre-commit
cfgv==3.5.0 ; python_full_version >= '3.10'
@ -76,6 +78,8 @@ contourpy==1.3.2 ; python_full_version == '3.10.*'
# via matplotlib
contourpy==1.3.3 ; python_full_version >= '3.11'
# via matplotlib
cryptography==46.0.3 ; python_full_version >= '3.10'
# via pyjwt
cycler==0.12.1
# via matplotlib
defusedxml==0.7.1 ; python_full_version < '3.10'
@ -102,11 +106,11 @@ face==24.0.0
# via glom
filelock==3.19.1 ; python_full_version < '3.10'
# via virtualenv
filelock==3.20.0 ; python_full_version >= '3.10'
filelock==3.20.1 ; python_full_version >= '3.10'
# via virtualenv
fonttools==4.60.1 ; python_full_version < '3.10'
fonttools==4.60.2 ; python_full_version < '3.10'
# via matplotlib
fonttools==4.61.0 ; python_full_version >= '3.10'
fonttools==4.61.1 ; python_full_version >= '3.10'
# via matplotlib
glom==22.1.0
# via semgrep
@ -169,8 +173,8 @@ markdown-it-py==4.0.0 ; python_full_version >= '3.10'
markupsafe==3.0.3
# via jinja2
matplotlib==3.9.4 ; python_full_version < '3.10'
matplotlib==3.10.7 ; python_full_version >= '3.10'
mcp==1.16.0 ; python_full_version >= '3.10'
matplotlib==3.10.8 ; python_full_version >= '3.10'
mcp==1.23.3 ; python_full_version >= '3.10'
# via semgrep
mdurl==0.1.2
# via markdown-it-py
@ -278,7 +282,7 @@ platformdirs==4.5.1 ; python_full_version >= '3.10'
pluggy==1.6.0
# via pytest
pre-commit==4.3.0 ; python_full_version < '3.10'
pre-commit==4.5.0 ; python_full_version >= '3.10'
pre-commit==4.5.1 ; python_full_version >= '3.10'
# via
# googleapis-common-protos
# opentelemetry-proto
@ -290,7 +294,7 @@ protobuf==4.25.8 ; python_full_version < '3.10'
psutil==7.1.3
# via memory-profiler
py-spy==0.4.1
pycparser==2.23 ; implementation_name != 'PyPy' and implementation_name != 'pypy' and os_name == 'nt'
pycparser==2.23 ; (python_full_version >= '3.10' and implementation_name != 'PyPy' and os_name != 'nt' and platform_python_implementation != 'PyPy') or (python_full_version >= '3.10' and implementation_name == 'pypy' and os_name == 'nt' and platform_python_implementation != 'PyPy') or (implementation_name != 'PyPy' and implementation_name != 'pypy' and os_name == 'nt')
# via cffi
pydantic==2.12.5
# via
@ -307,13 +311,15 @@ pygments==2.19.2
# rich
# sphinx
pyinstrument==5.1.1
pyjwt==2.10.1 ; python_full_version >= '3.10'
# via mcp
pyparsing==3.2.5
# via matplotlib
pyright==1.1.407
pysocks==1.7.1
# via urllib3
pytest==8.4.2 ; python_full_version < '3.10'
pytest==9.0.1 ; python_full_version >= '3.10'
pytest==9.0.2 ; python_full_version >= '3.10'
python-dateutil==2.9.0.post0
# via matplotlib
python-debian==1.0.1
@ -324,7 +330,7 @@ python-dotenv==1.2.1
# webdriver-manager
python-magic==0.4.27 ; python_full_version >= '3.10'
# via reuse
python-multipart==0.0.20 ; python_full_version >= '3.10'
python-multipart==0.0.21 ; python_full_version >= '3.10'
# via mcp
pywin32==311 ; python_full_version >= '3.10' and sys_platform == 'win32'
# via
@ -353,7 +359,9 @@ rich==13.5.3
# via
# semgrep
# typer
roman-numerals-py==3.1.0 ; python_full_version >= '3.11'
roman-numerals==4.1.0 ; python_full_version >= '3.11'
# via roman-numerals-py
roman-numerals-py==4.1.0 ; python_full_version >= '3.11'
# via sphinx
rpds-py==0.27.1 ; python_full_version < '3.10'
# via
@ -372,11 +380,11 @@ ruamel-yaml-clib==0.2.14 ; python_full_version >= '3.10' or platform_python_impl
# via
# ruamel-yaml
# semgrep
ruff==0.14.8
ruff==0.14.10
selenium==4.36.0 ; python_full_version < '3.10'
selenium==4.38.0 ; python_full_version >= '3.10'
selenium==4.39.0 ; python_full_version >= '3.10'
semgrep==1.136.0 ; python_full_version < '3.10'
semgrep==1.145.0 ; python_full_version >= '3.10'
semgrep==1.146.0 ; python_full_version >= '3.10'
setuptools==80.9.0
# via opentelemetry-instrumentation
shellingham==1.5.4
@ -416,10 +424,12 @@ sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==2.0.0
# via sphinx
sse-starlette==3.0.3 ; python_full_version >= '3.10'
sse-starlette==3.0.4 ; python_full_version >= '3.10'
# via mcp
starlette==0.50.0 ; python_full_version >= '3.10'
# via mcp
# via
# mcp
# sse-starlette
tomli==2.0.2
# via
# pytest
@ -437,11 +447,13 @@ trio==0.32.0 ; python_full_version >= '3.10'
# trio-websocket
trio-websocket==0.12.2
# via selenium
typer==0.20.0
typer==0.20.1
# via rstcheck
typing-extensions==4.15.0
# via
# anyio
# cryptography
# mcp
# opentelemetry-api
# opentelemetry-exporter-otlp-proto-http
# opentelemetry-sdk
@ -459,9 +471,10 @@ typing-extensions==4.15.0
# virtualenv
typing-inspection==0.4.2
# via
# mcp
# pydantic
# pydantic-settings
urllib3==2.6.0
urllib3==2.6.2
# via
# requests
# selenium

View File

@ -20,7 +20,7 @@ docker==7.1.0
# via glances
exceptiongroup==1.2.2 ; python_full_version < '3.11'
# via anyio
fastapi==0.124.0
fastapi==0.125.0
# via glances
h11==0.16.0
# via uvicorn
@ -74,7 +74,7 @@ typing-extensions==4.15.0
# uvicorn
typing-inspection==0.4.2
# via pydantic
urllib3==2.6.0
urllib3==2.6.2
# via
# docker
# podman

View File

@ -66,7 +66,7 @@ class Amp(GlancesAmp):
"""Update the AMP"""
# Get the Nginx status
logger.debug('{}: Update stats using status URL {}'.format(self.NAME, self.get('status_url')))
res = requests.get(self.get('status_url'))
res = requests.get(self.get('status_url'), timeout=15)
if res.ok:
# u'Active connections: 1 \nserver accepts handled requests\n 1 1 1 \nReading: 0 Writing: 1 Waiting: 0 \n'
self.set_result(res.text.rstrip())

View File

@ -9,7 +9,7 @@
"""CPU percent stats shared between CPU and Quicklook plugins."""
import platform
from typing import Optional, TypedDict
from typing import TypedDict
import psutil
@ -21,8 +21,8 @@ __all__ = ["cpu_percent"]
class CpuInfo(TypedDict):
cpu_name: str
cpu_hz: Optional[float]
cpu_hz_current: Optional[float]
cpu_hz: float | None
cpu_hz_current: float | None
class PerCpuPercentInfo(TypedDict):
@ -32,15 +32,15 @@ class PerCpuPercentInfo(TypedDict):
user: float
system: float
idle: float
nice: Optional[float]
iowait: Optional[float]
irq: Optional[float]
softirq: Optional[float]
steal: Optional[float]
guest: Optional[float]
guest_nice: Optional[float]
dpc: Optional[float]
interrupt: Optional[float]
nice: float | None
iowait: float | None
irq: float | None
softirq: float | None
steal: float | None
guest: float | None
guest_nice: float | None
dpc: float | None
interrupt: float | None
class CpuPercent:
@ -84,7 +84,7 @@ class CpuPercent:
self.cpu_info['cpu_hz_current'] = cpu_freq.current
else:
self.cpu_info['cpu_hz_current'] = None
if hasattr(cpu_freq, 'max'):
if hasattr(cpu_freq, 'max') and cpu_freq.max != 0.0:
self.cpu_info['cpu_hz'] = cpu_freq.max
else:
self.cpu_info['cpu_hz'] = None

View File

@ -54,7 +54,7 @@ class Export(GlancesExport):
# One complete loop have been done
logger.debug(f"Export stats ({listkeys(self.buffer)}) to RESTful endpoint ({self.client})")
# Export stats
post(self.client, json=self.buffer, allow_redirects=True)
post(self.client, json=self.buffer, allow_redirects=True, timeout=15)
# Reset buffer
self.buffer = {}

View File

@ -364,7 +364,7 @@ def json_dumps(data) -> bytes:
return b(res)
def json_loads(data: Union[str, bytes, bytearray]) -> Union[dict, list]:
def json_loads(data: str | bytes | bytearray) -> dict | list:
"""Load a JSON buffer into memory as a Python object"""
return json.loads(data)
@ -401,7 +401,7 @@ def dictlist_json_dumps(data, item):
return json_dumps(dl)
def dictlist_first_key_value(data: list[dict], key, value) -> Optional[dict]:
def dictlist_first_key_value(data: list[dict], key, value) -> dict | None:
"""In a list of dict, return first item where key=value or none if not found."""
try:
ret = next(item for item in data if key in item and item[key] == value)

View File

@ -12,7 +12,7 @@ import os
import socket
import sys
import webbrowser
from typing import Annotated, Any, Union
from typing import Annotated, Any
from urllib.parse import urljoin
from glances import __apiversion__, __version__
@ -815,7 +815,7 @@ class GlancesRestfulApi:
else:
return GlancesJSONResponse(ret)
def _api_value(self, plugin: str, item: str, value: Union[str, int, float]):
def _api_value(self, plugin: str, item: str, value: str | int | float):
"""Glances API RESTful implementation.
Return the process stats (dict) for the given item=value

View File

@ -78,7 +78,7 @@ class GlancesStdoutFetch:
fetch_template = f.read()
# Create a Jinja2 environment
jinja_env = jinja2.Environment(loader=jinja2.BaseLoader())
jinja_env = jinja2.Environment(loader=jinja2.BaseLoader(), autoescape=True)
template = jinja_env.from_string(fetch_template)
output = template.render(gl=self.gl)
print(output)

File diff suppressed because it is too large Load Diff

View File

@ -1,34 +1,34 @@
{
"private": true,
"dependencies": {
"bootstrap": "^5.3.5",
"bootstrap": "^5.3.8",
"favico.js": "^0.3.10",
"hotkeys-js": "^3.13.9",
"hotkeys-js": "^3.13.15",
"lodash": "^4.17.21",
"sanitize-html": "^2.16.0",
"sanitize-html": "^2.17.0",
"vue": "^3.5.13"
},
"devDependencies": {
"@vue/compiler-sfc": "^3.5.13",
"copy-webpack-plugin": "^13.0.0",
"copy-webpack-plugin": "^13.0.1",
"css-loader": "^7.1.2",
"del": "^8.0.0",
"eslint": "^9.25.0",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-vue": "^10.0.0",
"globals": "^16.0.0",
"html-webpack-plugin": "^5.6.3",
"less": "^4.3.0",
"less-loader": "^12.2.0",
"sass": "^1.86.3",
"sass-loader": "^16.0.5",
"del": "^8.0.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-vue": "^10.6.2",
"globals": "^16.5.0",
"html-webpack-plugin": "^5.6.5",
"less": "^4.5.1",
"less-loader": "^12.3.0",
"sass": "^1.97.1",
"sass-loader": "^16.0.6",
"style-loader": "^4.0.0",
"typescript-eslint": "^8.30.1",
"typescript-eslint": "^8.50.0",
"url-loader": "^4.1.1",
"vue-loader": "^17.4.2",
"webpack": "^5.99.6",
"webpack": "^5.104.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.1"
"webpack-dev-server": "^5.2.2"
},
"scripts": {
"build": "webpack --progress --mode=production",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -11,7 +11,7 @@
from copy import deepcopy
from functools import partial, reduce
from itertools import chain
from typing import Any, Optional
from typing import Any
from glances.globals import nativestr
from glances.logger import logger
@ -514,7 +514,7 @@ class ContainersPlugin(GlancesPluginModel):
return ret
def msg_curse(self, args=None, max_width: Optional[int] = None) -> list[str]:
def msg_curse(self, args=None, max_width: int | None = None) -> list[str]:
"""Return the dict to display in the curse interface."""
# Init the return message
init = []
@ -594,7 +594,6 @@ def sort_docker_stats(stats: list[dict[str, Any]]) -> tuple[str, list[dict[str,
# Return the main sort key and the sorted stats
return sort_by, stats
# Return the main sort key and the sorted stats
return sort_by, stats
# Return the main sort key and the sorted stats
return sort_by, stats
# End of file

View File

@ -9,7 +9,7 @@
"""Docker Extension unit for Glances' Containers plugin."""
import time
from typing import Any, Optional
from typing import Any
from glances.globals import nativestr, pretty_date, replace_special_chars
from glances.logger import logger
@ -83,7 +83,7 @@ class DockerStatsFetcher:
# In case no update, default to 1
return max(1, self._streamer.last_update_time - self._last_stats_computed_time)
def _get_cpu_stats(self) -> Optional[dict[str, float]]:
def _get_cpu_stats(self) -> dict[str, float] | None:
"""Return the container CPU usage.
Output: a dict {'total': 1.49}
@ -117,7 +117,7 @@ class DockerStatsFetcher:
# Return the stats
return stats
def _get_memory_stats(self) -> Optional[dict[str, float]]:
def _get_memory_stats(self) -> dict[str, float] | None:
"""Return the container MEMORY.
Output: a dict {'usage': ..., 'limit': ..., 'inactive_file': ...}
@ -140,7 +140,7 @@ class DockerStatsFetcher:
# Return the stats
return stats
def _get_network_stats(self) -> Optional[dict[str, float]]:
def _get_network_stats(self) -> dict[str, float] | None:
"""Return the container network usage using the Docker API (v1.0 or higher).
Output: a dict {'time_since_update': 3000, 'rx': 10, 'tx': 65}.
@ -169,7 +169,7 @@ class DockerStatsFetcher:
# Return the stats
return stats
def _get_io_stats(self) -> Optional[dict[str, float]]:
def _get_io_stats(self) -> dict[str, float] | None:
"""Return the container IO usage using the Docker API (v1.0 or higher).
Output: a dict {'time_since_update': 3000, 'ior': 10, 'iow': 65}.

View File

@ -9,7 +9,7 @@
import time
from datetime import datetime
from typing import Any, Optional
from typing import Any
from glances.globals import nativestr, pretty_date, replace_special_chars, string_value_to_float
from glances.logger import logger
@ -164,7 +164,7 @@ class PodmanPodStatsFetcher:
return result_stats
def _get_cpu_stats(self, stats: dict) -> Optional[dict]:
def _get_cpu_stats(self, stats: dict) -> dict | None:
"""Return the container CPU usage.
Output: a dict {'total': 1.49}
@ -176,7 +176,7 @@ class PodmanPodStatsFetcher:
cpu_usage = string_value_to_float(stats["CPU"].rstrip("%"))
return {"total": cpu_usage}
def _get_memory_stats(self, stats) -> Optional[dict]:
def _get_memory_stats(self, stats) -> dict | None:
"""Return the container MEMORY.
Output: a dict {'usage': ..., 'limit': ...}
@ -197,7 +197,7 @@ class PodmanPodStatsFetcher:
return {'usage': usage, 'limit': limit, 'inactive_file': 0}
def _get_network_stats(self, stats) -> Optional[dict]:
def _get_network_stats(self, stats) -> dict | None:
"""Return the container network usage using the Docker API (v1.0 or higher).
Output: a dict {'time_since_update': 3000, 'rx': 10, 'tx': 65}.
@ -223,7 +223,7 @@ class PodmanPodStatsFetcher:
# Hardcode `time_since_update` to 1 as podman docs don't specify the rate calculation procedure
return {"rx": rx, "tx": tx, "time_since_update": 1}
def _get_io_stats(self, stats) -> Optional[dict]:
def _get_io_stats(self, stats) -> dict | None:
"""Return the container IO usage using the Docker API (v1.0 or higher).
Output: a dict {'time_since_update': 3000, 'ior': 10, 'iow': 65}.

View File

@ -15,6 +15,7 @@ Currently supported:
"""
from glances.globals import to_fahrenheit
from glances.logger import logger
from glances.plugins.gpu.cards.amd import AmdGPU
from glances.plugins.gpu.cards.nvidia import NvidiaGPU
from glances.plugins.plugin.model import GlancesPluginModel
@ -73,11 +74,21 @@ class GpuPlugin(GlancesPluginModel):
stats_init_value=[],
fields_description=fields_description,
)
# Init the GPU API
self.nvidia = NvidiaGPU()
self.amd = AmdGPU()
# Init the Nvidia GPU API
try:
self.nvidia = NvidiaGPU()
except Exception as e:
logger.debug(f'Nvidia GPU initialization error: {e}')
self.nvidia = None
# Init the AMD GPU API
# Just for test purpose (uncomment to test on computer without AMD GPU)
# self.amd = AmdGPU(drm_root_folder='./tests-data/plugins/gpu/amd/sys/class/drm')
try:
self.amd = AmdGPU()
except Exception as e:
logger.debug(f'AMD GPU initialization error: {e}')
self.amd = None
# We want to display the stat in the curse interface
self.display_curse = True
@ -102,11 +113,13 @@ class GpuPlugin(GlancesPluginModel):
stats = self.get_init_value()
# Get the stats
stats.extend(self.nvidia.get_device_stats())
stats.extend(self.amd.get_device_stats())
if self.nvidia:
stats.extend(self.nvidia.get_device_stats())
if self.amd:
stats.extend(self.amd.get_device_stats())
# !!!
# Uncomment to test on computer without GPU
# Uncomment to test on computer without Nvidia GPU
# One GPU sample:
# stats = [
# {

View File

@ -24,7 +24,10 @@ See: https://wiki.archlinux.org/title/AMDGPU#Manually
# │   ├── gpu_busy_percent
# │   ├── hwmon
# │   │   └── hwmon0
# │   │   ├── in1_input
# │   │   └── temp1_input
# │   ├── mem_info_gtt_total
# │   ├── mem_info_gtt_used
# │   ├── mem_info_vram_total
# │   ├── mem_info_vram_used
# │   ├── pp_dpm_mclk
@ -37,21 +40,22 @@ See: https://wiki.archlinux.org/title/AMDGPU#Manually
# └── amdgpu_pm_info
import functools
import glob
import os
import re
from typing import Optional
DRM_ROOT_FOLDER: str = '/sys/class/drm'
CARD_REGEX: str = r"^card\d$"
DEVICE_FOLDER: str = 'device'
DEVICE_FOLDER_PATTERN: str = 'card[0-9]/device'
AMDGPU_IDS_FILE: str = '/usr/share/libdrm/amdgpu.ids'
PCI_DEVICE_ID: str = 'device'
PCI_REVISION_ID: str = 'revision'
GPU_PROC_PERCENT: str = 'gpu_busy_percent'
GPU_MEM_TOTAL: str = 'mem_info_vram_total'
GPU_MEM_USED: str = 'mem_info_vram_used'
HWMON_REGEXP: str = r"^hwmon\d$"
GPU_TEMPERATURE_REGEXP: str = r"^temp\d_input"
GTT_MEM_TOTAL: str = 'mem_info_gtt_total'
GTT_MEM_USED: str = 'mem_info_gtt_used'
HWMON_NORTHBRIDGE_VOLTAGE_PATTERN: str = 'hwmon/hwmon[0-9]/in1_input'
HWMON_TEMPERATURE_PATTERN = 'hwmon/hwmon[0-9]/temp[0-9]_input'
class AmdGPU:
@ -90,22 +94,17 @@ class AmdGPU:
return stats
def get_device_list(drm_root_folder: str) -> list:
def get_device_list(drm_root_folder: str) -> list[str]:
"""Return a list of path to the device stats."""
ret = []
for root, dirs, _ in os.walk(drm_root_folder):
for d in dirs:
if (
re.match(CARD_REGEX, d)
and DEVICE_FOLDER in os.listdir(os.path.join(root, d))
and os.path.isfile(os.path.join(root, d, DEVICE_FOLDER, GPU_PROC_PERCENT))
):
# If the GPU busy file is present then take the card into account
ret.append(os.path.join(root, d, DEVICE_FOLDER))
for device_folder in glob.glob(DEVICE_FOLDER_PATTERN, root_dir=drm_root_folder):
if os.path.isfile(os.path.join(drm_root_folder, device_folder, GPU_PROC_PERCENT)):
# If the GPU busy file is present then take the card into account
ret.append(os.path.join(drm_root_folder, device_folder))
return ret
def read_file(*path_segments: str) -> Optional[str]:
def read_file(*path_segments: str) -> str | None:
"""Return content of file."""
path = os.path.join(*path_segments)
if os.path.isfile(path):
@ -139,43 +138,47 @@ def get_device_name(device_folder: str) -> str:
return 'AMD GPU'
def get_mem(device_folder: str) -> Optional[int]:
def get_mem(device_folder: str) -> int | None:
"""Return the memory consumption in %."""
mem_info_vram_total = read_file(device_folder, GPU_MEM_TOTAL)
mem_info_vram_used = read_file(device_folder, GPU_MEM_USED)
if mem_info_vram_total and mem_info_vram_used:
mem_info_vram_total = int(mem_info_vram_total)
mem_info_vram_used = int(mem_info_vram_used)
if mem_info_vram_total > 0:
return round(mem_info_vram_used / mem_info_vram_total * 100)
mem_info_total = read_file(device_folder, GPU_MEM_TOTAL)
mem_info_used = read_file(device_folder, GPU_MEM_USED)
if mem_info_total and mem_info_used:
mem_info_total = int(mem_info_total)
mem_info_used = int(mem_info_used)
# Detect integrated GPU by looking for APU-only Northbridge voltage.
# See https://docs.kernel.org/gpu/amdgpu/thermal.html
if glob.glob(HWMON_NORTHBRIDGE_VOLTAGE_PATTERN, root_dir=device_folder):
mem_info_gtt_total = read_file(device_folder, GTT_MEM_TOTAL)
mem_info_gtt_used = read_file(device_folder, GTT_MEM_USED)
if mem_info_gtt_total and mem_info_gtt_used:
# Integrated GPU allocates static VRAM and dynamic GTT from the same system memory.
mem_info_total += int(mem_info_gtt_total)
mem_info_used += int(mem_info_gtt_used)
if mem_info_total > 0:
return round(mem_info_used / mem_info_total * 100)
return None
def get_proc(device_folder: str) -> Optional[int]:
def get_proc(device_folder: str) -> int | None:
"""Return the processor consumption in %."""
if gpu_busy_percent := read_file(device_folder, GPU_PROC_PERCENT):
return int(gpu_busy_percent)
return None
def get_temperature(device_folder: str) -> Optional[int]:
def get_temperature(device_folder: str) -> int | None:
"""Return the processor temperature in °C (mean of all HWMON)"""
temp_input = []
for root, dirs, _ in os.walk(device_folder):
for d in dirs:
if re.match(HWMON_REGEXP, d):
for _, _, files in os.walk(os.path.join(root, d)):
for f in files:
if re.match(GPU_TEMPERATURE_REGEXP, f):
if a_temp_input := read_file(root, d, f):
temp_input.append(int(a_temp_input))
else:
return None
for temp_file in glob.glob(HWMON_TEMPERATURE_PATTERN, root_dir=device_folder):
if a_temp_input := read_file(device_folder, temp_file):
temp_input.append(int(a_temp_input))
else:
return None
if temp_input:
return round(sum(temp_input) / len(temp_input) / 1000)
return None
def get_fan_speed(device_folder: str) -> Optional[int]:
def get_fan_speed(device_folder: str) -> int | None:
"""Return the fan speed in %."""
return None

View File

@ -98,11 +98,11 @@ class LoadPlugin(GlancesPluginModel):
# Update stats using the standard system lib
# Get the load using the os standard lib
load = get_load_average()
load = load_average()
if load is None:
stats = self.get_init_value()
else:
stats = {'min1': load[0], 'min5': load[1], 'min15': load[2], 'cpucore': get_nb_log_core()}
stats = {'min1': load[0], 'min5': load[1], 'min15': load[2], 'cpucore': log_core()}
elif self.input_method == 'snmp':
# Update stats using SNMP
@ -116,7 +116,7 @@ class LoadPlugin(GlancesPluginModel):
for k, v in stats.items():
stats[k] = float(v)
stats['cpucore'] = get_nb_log_core()
stats['cpucore'] = log_core()
# Update the stats
self.stats = stats
@ -164,9 +164,9 @@ class LoadPlugin(GlancesPluginModel):
ret.append(self.curse_new_line())
msg = '{:7}'.format(f'{load_time} min')
ret.append(self.curse_add_line(msg))
if args.disable_irix and get_nb_log_core() != 0:
if args.disable_irix and log_core() != 0:
# Enable Irix mode for load (see issue #1554)
load_stat = self.stats[f'min{load_time}'] / get_nb_log_core() * 100
load_stat = self.stats[f'min{load_time}'] / log_core() * 100
msg = f'{load_stat:>5.1f}%'
else:
# Default mode for load
@ -177,30 +177,30 @@ class LoadPlugin(GlancesPluginModel):
return ret
def get_nb_log_core():
def log_core():
"""Get the number of logical CPU core."""
return nb_log_core
def get_nb_phys_core():
def phys_core():
"""Get the number of physical CPU core."""
return nb_phys_core
def get_load_average(percent: bool = False):
def load_average(percent: bool = False):
"""Get load average. On both Linux and Windows thanks to PsUtil
if percent is True, return the load average in percent
Ex: if you only have one CPU core and the load average is 1.0, then return 100%"""
load_average = None
ret = None
try:
load_average = psutil.getloadavg()
ret = psutil.getloadavg()
except (AttributeError, OSError):
try:
load_average = os.getloadavg()
ret = os.getloadavg()
except (AttributeError, OSError):
pass
if load_average and percent:
return tuple([round(i / get_nb_log_core() * 100, 1) for i in load_average])
return load_average
if ret and percent:
return tuple([round(i / log_core() * 100, 1) for i in ret])
return ret

View File

@ -15,7 +15,7 @@ from glances.logger import logger
from glances.outputs.glances_bars import Bar
from glances.outputs.glances_sparklines import Sparkline
from glances.plugins.fs.zfs import zfs_enable, zfs_stats
from glances.plugins.load import get_load_average, get_nb_log_core, get_nb_phys_core
from glances.plugins.load import load_average, log_core, phys_core
from glances.plugins.plugin.model import GlancesPluginModel
# Fields description
@ -144,12 +144,12 @@ class QuicklookPlugin(GlancesPluginModel):
stats['swap'] = None
# Get load
stats['cpu_log_core'] = get_nb_log_core()
stats['cpu_phys_core'] = get_nb_phys_core()
stats['cpu_log_core'] = log_core()
stats['cpu_phys_core'] = phys_core()
try:
# Load average is a tuple (1 min, 5 min, 15 min)
# Process only the 15 min value (index 2)
stats['load'] = get_load_average(percent=True)[2]
stats['load'] = load_average(percent=True)[2]
except (TypeError, IndexError):
stats['load'] = None
@ -202,15 +202,13 @@ class QuicklookPlugin(GlancesPluginModel):
##########################
# System information
if (
'cpu_hz_current' in self.stats
and self.stats['cpu_hz_current'] is not None
and 'cpu_hz' in self.stats
and self.stats['cpu_hz'] is not None
):
msg_freq = ' {:.2f}/{:.2f}GHz'.format(
self._hz_to_ghz(self.stats['cpu_hz_current']), self._hz_to_ghz(self.stats['cpu_hz'])
)
if 'cpu_hz_current' in self.stats and self.stats['cpu_hz_current'] is not None:
if 'cpu_hz' in self.stats and self.stats['cpu_hz'] is not None:
msg_freq = ' {:.2f}/{:.2f}GHz'.format(
self._hz_to_ghz(self.stats['cpu_hz_current']), self._hz_to_ghz(self.stats['cpu_hz'])
)
else:
msg_freq = ' {:.2f}GHz'.format(self._hz_to_ghz(self.stats['cpu_hz_current']))
else:
msg_freq = ''
@ -320,3 +318,6 @@ class QuicklookPlugin(GlancesPluginModel):
def _mhz_to_hz(self, hz):
"""Convert Mhz to Hz."""
return hz * 1000000.0
# End of file

View File

@ -9,7 +9,7 @@
"""Vms plugin."""
from copy import deepcopy
from typing import Any, Optional
from typing import Any
from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel
@ -201,7 +201,7 @@ class VmsPlugin(GlancesPluginModel):
return True
def msg_curse(self, args=None, max_width: Optional[int] = None) -> list[str]:
def msg_curse(self, args=None, max_width: int | None = None) -> list[str]:
"""Return the dict to display in the curse interface."""
# Init the return message
ret = []
@ -345,3 +345,6 @@ def sort_vm_stats(stats: list[dict[str, Any]]) -> tuple[str, list[dict[str, Any]
# Return the main sort key and the sorted stats
return sort_by, stats
# End of file

View File

@ -8,10 +8,8 @@ classifiers = [
"Intended Audience :: Developers",
"Intended Audience :: End Users/Desktop",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
@ -30,10 +28,10 @@ dependencies = [
description = "A cross-platform curses-based monitoring tool"
dynamic = ["version"]
keywords = ["cli", "curses", "monitoring", "system"]
license = {text = "LGPLv3"}
license = "LGPL-3.0-only"
name = "Glances"
readme = "README-pypi.rst"
requires-python = ">=3.9"
requires-python = ">=3.10"
urls.Homepage = "https://github.com/nicolargo/glances"
[dependency-groups]
@ -136,7 +134,7 @@ include = ["glances*"]
[tool.ruff]
line-length = 120
target-version = "py39"
target-version = "py310"
[tool.ruff.format]
quote-style = "preserve"

View File

@ -0,0 +1 @@
17179869184

View File

@ -0,0 +1 @@
79949824