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

This commit is contained in:
nicolargo 2024-05-18 09:32:32 +02:00
commit 5262524db3
11 changed files with 1377 additions and 1216 deletions

File diff suppressed because it is too large Load Diff

View File

@ -32,7 +32,13 @@ Item (or event) is defined by:
} }
""" """
from pydantic.dataclasses import dataclass from glances.logger import logger
try:
from pydantic.dataclasses import dataclass
except ImportError as e:
logger.warning(f"Missing Python Lib ({e}), EventList will be skipping data validation")
from dataclasses import dataclass
from glances.processes import sort_stats from glances.processes import sort_stats

View File

@ -11,7 +11,7 @@
import time import time
from datetime import datetime from datetime import datetime
from pydantic import RootModel from dataclasses import asdict
from glances.processes import glances_processes from glances.processes import glances_processes
from glances.thresholds import glances_thresholds from glances.thresholds import glances_thresholds
@ -201,7 +201,7 @@ class GlancesEventsList(object):
def get(self): def get(self):
"""Return the RAW events list.""" """Return the RAW events list."""
return [RootModel[GlancesEvent](e).model_dump() for e in self.events_list] return [asdict(e) for e in self.events_list]
def len(self): def len(self):
"""Return the number of events in the logs list.""" """Return the number of events in the logs list."""

View File

@ -18,7 +18,7 @@ from glances.exports.export import GlancesExport
from influxdb import InfluxDBClient from influxdb import InfluxDBClient
from influxdb.client import InfluxDBClientError from influxdb.client import InfluxDBClientError
FIELD_TO_TAG = ['name', 'cmdline'] FIELD_TO_TAG = ['name', 'cmdline', 'type']
class Export(GlancesExport): class Export(GlancesExport):

View File

@ -17,7 +17,7 @@ from glances.exports.export import GlancesExport
from influxdb_client import InfluxDBClient, WriteOptions from influxdb_client import InfluxDBClient, WriteOptions
FIELD_TO_TAG = ['name', 'cmdline'] FIELD_TO_TAG = ['name', 'cmdline', 'type']
class Export(GlancesExport): class Export(GlancesExport):

View File

@ -287,10 +287,13 @@ class _GlancesCurses(object):
self.filter_color = curses.color_pair(9) | A_BOLD self.filter_color = curses.color_pair(9) | A_BOLD
self.selected_color = curses.color_pair(10) | A_BOLD self.selected_color = curses.color_pair(10) | A_BOLD
# Define separator line style # Define separator line style
curses.init_color(11, 500, 500, 500) try:
curses.init_pair(11, curses.COLOR_BLACK, -1) curses.init_color(11, 500, 500, 500)
self.separator = curses.color_pair(11) curses.init_pair(11, curses.COLOR_BLACK, -1)
self.separator = curses.color_pair(11)
except Exception:
# Catch exception in TMUX
pass
else: else:
# The screen is NOT compatible with a colored design # The screen is NOT compatible with a colored design
# switch to B&W text styles # switch to B&W text styles

View File

@ -16,8 +16,12 @@ from io import open
import webbrowser import webbrowser
from urllib.parse import urljoin from urllib.parse import urljoin
# Replace typing_extensions by typing when Python 3.8 support will be dropped try:
from typing import Annotated from typing import Annotated
except ImportError:
# Only for Python 3.8
# To be removed when Python 3.8 support will be dropped
from typing_extensions import Annotated
from glances import __version__, __apiversion__ from glances import __version__, __apiversion__
from glances.password import GlancesPassword from glances.password import GlancesPassword

View File

@ -8,30 +8,34 @@
# #
"""Sensors plugin.""" """Sensors plugin."""
from enum import Enum
from concurrent.futures import ThreadPoolExecutor
from typing import List, Dict, Literal, Any
import psutil import psutil
import warnings import warnings
import threading
from glances.logger import logger from glances.logger import logger
from glances.globals import iteritems, to_fahrenheit from glances.globals import to_fahrenheit
from glances.timer import Counter from glances.timer import Counter
from glances.plugins.sensors.sensor.glances_batpercent import PluginModel as BatPercentPluginModel from glances.plugins.sensors.sensor.glances_batpercent import PluginModel as BatPercentPluginModel
from glances.plugins.sensors.sensor.glances_hddtemp import PluginModel as HddTempPluginModel from glances.plugins.sensors.sensor.glances_hddtemp import PluginModel as HddTempPluginModel
from glances.outputs.glances_unicode import unicode_message from glances.outputs.glances_unicode import unicode_message
from glances.plugins.plugin.model import GlancesPluginModel from glances.plugins.plugin.model import GlancesPluginModel
SENSOR_TEMP_TYPE = 'temperature_core'
SENSOR_TEMP_UNIT = 'C'
SENSOR_FAN_TYPE = 'fan_speed' class SensorType(str, Enum):
SENSOR_FAN_UNIT = 'R' # Switch to `enum.StrEnum` when we only support py311+
CPU_TEMP = 'temperature_core'
FAN_SPEED = 'fan_speed'
HDD_TEMP = 'temperature_hdd'
BATTERY = 'battery'
SENSOR_HDDTEMP_TYPE = 'temperature_hdd'
SENSOR_HDDTEMP_UNIT = 'C'
SENSORS_BATTERY_TYPE = 'battery' CPU_TEMP_UNIT = 'C'
SENSORS_BATTERY_UNIT = '%' FAN_SPEED_UNIT = 'R'
HDD_TEMP_UNIT = 'C'
BATTERY_UNIT = '%'
# Define the default refresh multiplicator # Define the default refresh multiplicator
# Default value is 3 * Glances refresh time # Default value is 3 * Glances refresh time
@ -82,28 +86,38 @@ class PluginModel(GlancesPluginModel):
super(PluginModel, self).__init__( super(PluginModel, self).__init__(
args=args, config=config, stats_init_value=[], fields_description=fields_description args=args, config=config, stats_init_value=[], fields_description=fields_description
) )
start_duration = Counter() start_duration = Counter()
# Init the sensor class # Init the sensor class
start_duration.reset() start_duration.reset()
# Hotfix! Refactor to use only one `GlancesGrabSensors` later glances_grab_sensors_cpu_temp = GlancesGrabSensors(SensorType.CPU_TEMP)
self.glances_grab_sensors_fan_speed = GlancesGrabSensors() logger.debug("CPU Temp sensor plugin init duration: {} seconds".format(start_duration.get()))
self.glances_grab_sensors_temperature = GlancesGrabSensors()
logger.debug("Generic sensor plugin init duration: {} seconds".format(start_duration.get()))
# Instance for the HDDTemp Plugin in order to display the hard disks
# temperatures
start_duration.reset() start_duration.reset()
self.hddtemp_plugin = HddTempPluginModel(args=args, config=config) glances_grab_sensors_fan_speed = GlancesGrabSensors(SensorType.FAN_SPEED)
logger.debug("Fan speed sensor plugin init duration: {} seconds".format(start_duration.get()))
# Instance for the HDDTemp Plugin in order to display the hard disks temperatures
start_duration.reset()
hddtemp_plugin = HddTempPluginModel(args=args, config=config)
logger.debug("HDDTemp sensor plugin init duration: {} seconds".format(start_duration.get())) logger.debug("HDDTemp sensor plugin init duration: {} seconds".format(start_duration.get()))
# Instance for the BatPercent in order to display the batteries # Instance for the BatPercent in order to display the batteries capacities
# capacities
start_duration.reset() start_duration.reset()
self.batpercent_plugin = BatPercentPluginModel(args=args, config=config) batpercent_plugin = BatPercentPluginModel(args=args, config=config)
logger.debug("Battery sensor plugin init duration: {} seconds".format(start_duration.get())) logger.debug("Battery sensor plugin init duration: {} seconds".format(start_duration.get()))
self.sensors_grab_map: Dict[SensorType, Any] = {}
if glances_grab_sensors_cpu_temp.init:
self.sensors_grab_map[SensorType.CPU_TEMP] = glances_grab_sensors_cpu_temp
if glances_grab_sensors_fan_speed.init:
self.sensors_grab_map[SensorType.FAN_SPEED] = glances_grab_sensors_fan_speed
self.sensors_grab_map[SensorType.HDD_TEMP] = hddtemp_plugin
self.sensors_grab_map[SensorType.BATTERY] = batpercent_plugin
# We want to display the stat in the curse interface # We want to display the stat in the curse interface
self.display_curse = True self.display_curse = True
@ -115,37 +129,15 @@ class PluginModel(GlancesPluginModel):
"""Return the key of the list.""" """Return the key of the list."""
return 'label' return 'label'
def __get_temperature(self, stats, index): def __get_sensor_data(self, sensor_type: SensorType) -> List[Dict]:
try: try:
temperature = self.__set_type(self.glances_grab_sensors_temperature.get(SENSOR_TEMP_TYPE), SENSOR_TEMP_TYPE) data = self.sensors_grab_map[sensor_type].update()
data = self.__set_type(data, sensor_type)
except Exception as e: except Exception as e:
logger.error("Cannot grab sensors temperatures (%s)" % e) logger.error(f"Cannot grab sensors `{sensor_type}` ({e})")
return []
else: else:
stats[index] = self.__transform_sensors(temperature) return self.__transform_sensors(data)
def __get_fan_speed(self, stats, index):
try:
fan_speed = self.__set_type(self.glances_grab_sensors_fan_speed.get(SENSOR_FAN_TYPE), SENSOR_FAN_TYPE)
except Exception as e:
logger.error("Cannot grab FAN speed (%s)" % e)
else:
stats[index] = self.__transform_sensors(fan_speed)
def __get_hddtemp(self, stats, index):
try:
hddtemp = self.__set_type(self.hddtemp_plugin.update(), SENSOR_HDDTEMP_TYPE)
except Exception as e:
logger.error("Cannot grab HDD temperature (%s)" % e)
else:
stats[index] = self.__transform_sensors(hddtemp)
def __get_bat_percent(self, stats, index):
try:
bat_percent = self.__set_type(self.batpercent_plugin.update(), SENSORS_BATTERY_TYPE)
except Exception as e:
logger.error("Cannot grab battery percent (%s)" % e)
else:
stats[index] = self.__transform_sensors(bat_percent)
def __transform_sensors(self, threads_stats): def __transform_sensors(self, threads_stats):
"""Hide, alias and sort the result""" """Hide, alias and sort the result"""
@ -172,22 +164,17 @@ class PluginModel(GlancesPluginModel):
stats = self.get_init_value() stats = self.get_init_value()
if self.input_method == 'local': if self.input_method == 'local':
threads_stats = [None] * 4 with ThreadPoolExecutor(max_workers=len(self.sensors_grab_map)) as executor:
threads = [ logger.debug(f"Sensors enabled sub plugins: {list(self.sensors_grab_map.keys())}")
threading.Thread(name=SENSOR_TEMP_TYPE, target=self.__get_temperature, args=(threads_stats, 0)), futures = {t: executor.submit(self.__get_sensor_data, t) for t in self.sensors_grab_map.keys()}
threading.Thread(name=SENSOR_FAN_TYPE, target=self.__get_fan_speed, args=(threads_stats, 1)),
threading.Thread(name=SENSOR_HDDTEMP_TYPE, target=self.__get_hddtemp, args=(threads_stats, 2)),
threading.Thread(name=SENSORS_BATTERY_TYPE, target=self.__get_bat_percent, args=(threads_stats, 3)),
]
# Start threads in //
for t in threads:
t.start()
# Wait threads are finished
for t in threads:
t.join()
# Merge the results # Merge the results
for s in threads_stats: for sensor_type, future in futures.items():
stats.extend(s) try:
stats.extend(future.result())
except Exception as e:
logger.error(f"Cannot parse sensors data for `{sensor_type}` ({e})")
elif self.input_method == 'snmp': elif self.input_method == 'snmp':
# Update stats using SNMP # Update stats using SNMP
# No standard: # No standard:
@ -209,7 +196,7 @@ class PluginModel(GlancesPluginModel):
else: else:
return stats["label"] return stats["label"]
def __set_type(self, stats, sensor_type): def __set_type(self, stats: List[Dict[str, Any]], sensor_type: SensorType) -> List[Dict[str, Any]]:
"""Set the plugin type. """Set the plugin type.
4 types of stats is possible in the sensors plugin: 4 types of stats is possible in the sensors plugin:
@ -237,10 +224,10 @@ class PluginModel(GlancesPluginModel):
if not i['value']: if not i['value']:
continue continue
# Alert processing # Alert processing
if i['type'] == SENSOR_TEMP_TYPE: if i['type'] == SensorType.CPU_TEMP:
if self.is_limit('critical', stat_name=SENSOR_TEMP_TYPE + '_' + i['label']): if self.is_limit('critical', stat_name=SensorType.CPU_TEMP + '_' + i['label']):
# By default use the thresholds configured in the glances.conf file (see #2058) # By default use the thresholds configured in the glances.conf file (see #2058)
alert = self.get_alert(current=i['value'], header=SENSOR_TEMP_TYPE + '_' + i['label']) alert = self.get_alert(current=i['value'], header=SensorType.CPU_TEMP + '_' + i['label'])
else: else:
# Else use the system thresholds # Else use the system thresholds
if i['critical'] is None: if i['critical'] is None:
@ -253,7 +240,7 @@ class PluginModel(GlancesPluginModel):
alert = 'WARNING' alert = 'WARNING'
else: else:
alert = 'OK' alert = 'OK'
elif i['type'] == SENSORS_BATTERY_TYPE: elif i['type'] == SensorType.BATTERY:
# Battery is in % # Battery is in %
alert = self.get_alert(current=100 - i['value'], header=i['type']) alert = self.get_alert(current=100 - i['value'], header=i['type'])
else: else:
@ -297,7 +284,7 @@ class PluginModel(GlancesPluginModel):
# Stats # Stats
for i in self.stats: for i in self.stats:
# Do not display anything if no battery are detected # Do not display anything if no battery are detected
if i['type'] == SENSORS_BATTERY_TYPE and i['value'] == []: if i['type'] == SensorType.BATTERY and i['value'] == []:
continue continue
# New line # New line
ret.append(self.curse_new_line()) ret.append(self.curse_new_line())
@ -309,7 +296,7 @@ class PluginModel(GlancesPluginModel):
self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='value', option='decoration')) self.curse_add_line(msg, self.get_views(item=i[self.get_key()], key='value', option='decoration'))
) )
else: else:
if args.fahrenheit and i['type'] != SENSORS_BATTERY_TYPE and i['type'] != SENSOR_FAN_TYPE: if args.fahrenheit and i['type'] != SensorType.BATTERY and i['type'] != SensorType.FAN_SPEED:
trend = '' trend = ''
value = to_fahrenheit(i['value']) value = to_fahrenheit(i['value'])
unit = 'F' unit = 'F'
@ -334,73 +321,42 @@ class PluginModel(GlancesPluginModel):
class GlancesGrabSensors(object): class GlancesGrabSensors(object):
"""Get sensors stats.""" """Get sensors stats."""
def __init__(self): def __init__(self, sensor_type: Literal[SensorType.FAN_SPEED, SensorType.CPU_TEMP]):
"""Init sensors stats.""" """Init sensors stats."""
# Temperatures self.sensor_type = sensor_type
self.init_temp = False self.sensor_unit = CPU_TEMP_UNIT if self.sensor_type == SensorType.CPU_TEMP else FAN_SPEED_UNIT
self.sensor_temps = {}
self.init = False
try: try:
# psutil>=5.1.0, Linux-only self.__fetch_psutil()
self.sensor_temps = psutil.sensors_temperatures() self.init = True
except AttributeError: except AttributeError:
logger.debug("Cannot grab temperatures. Platform not supported.") logger.debug(f"Cannot grab {sensor_type}. Platform not supported.")
else:
self.init_temp = True def __fetch_psutil(self) -> Dict[str, list]:
if self.sensor_type == SensorType.CPU_TEMP:
# Solve an issue #1203 concerning a RunTimeError warning message displayed # Solve an issue #1203 concerning a RunTimeError warning message displayed
# in the curses interface. # in the curses interface.
warnings.filterwarnings("ignore") warnings.filterwarnings("ignore")
# Fans # psutil>=5.1.0, Linux-only
self.init_fan = False return psutil.sensors_temperatures()
self.sensor_fans = {}
try: if self.sensor_type == SensorType.FAN_SPEED:
# psutil>=5.2.0, Linux-only # psutil>=5.2.0, Linux-only
self.sensor_fans = psutil.sensors_fans() return psutil.sensors_fans()
except AttributeError:
logger.debug("Cannot grab fans speed. Platform not supported.")
else:
self.init_fan = True
# Init the stats raise ValueError(f"Unsupported sensor_type: {self.sensor_type}")
self.reset()
def reset(self): def update(self) -> List[dict]:
"""Reset/init the stats."""
self.sensors_list = []
def __update__(self):
"""Update the stats.""" """Update the stats."""
# Reset the list if not self.init:
self.reset() return []
if not self.init_temp:
return self.sensors_list
# Temperatures sensors # Temperatures sensors
self.sensors_list.extend(self.build_sensors_list(SENSOR_TEMP_UNIT))
# Fans sensors
self.sensors_list.extend(self.build_sensors_list(SENSOR_FAN_UNIT))
return self.sensors_list
def build_sensors_list(self, type):
"""Build the sensors list depending of the type.
type: SENSOR_TEMP_UNIT or SENSOR_FAN_UNIT
output: a list
"""
ret = [] ret = []
if type == SENSOR_TEMP_UNIT and self.init_temp: data = self.__fetch_psutil()
input_list = self.sensor_temps for chip_name, chip in data.items():
self.sensor_temps = psutil.sensors_temperatures()
elif type == SENSOR_FAN_UNIT and self.init_fan:
input_list = self.sensor_fans
self.sensor_fans = psutil.sensors_fans()
else:
return ret
for chip_name, chip in iteritems(input_list):
label_index = 1 label_index = 1
for chip_name_index, feature in enumerate(chip): for chip_name_index, feature in enumerate(chip):
sensors_current = {} sensors_current = {}
@ -413,8 +369,9 @@ class GlancesGrabSensors(object):
else: else:
sensors_current['label'] = feature.label sensors_current['label'] = feature.label
# Sensors value, limit and unit # Sensors value, limit and unit
sensors_current['unit'] = type sensors_current['unit'] = self.sensor_unit
sensors_current['value'] = int(getattr(feature, 'current', 0) if getattr(feature, 'current', 0) else 0) sensors_current['value'] = int(
getattr(feature, 'current', 0) if getattr(feature, 'current', 0) else 0)
system_warning = getattr(feature, 'high', None) system_warning = getattr(feature, 'high', None)
system_critical = getattr(feature, 'critical', None) system_critical = getattr(feature, 'critical', None)
sensors_current['warning'] = int(system_warning) if system_warning is not None else None sensors_current['warning'] = int(system_warning) if system_warning is not None else None
@ -422,16 +379,3 @@ class GlancesGrabSensors(object):
# Add sensor to the list # Add sensor to the list
ret.append(sensors_current) ret.append(sensors_current)
return ret return ret
def get(self, sensor_type=SENSOR_TEMP_TYPE):
"""Get sensors list."""
self.__update__()
if sensor_type == SENSOR_TEMP_TYPE:
ret = [s for s in self.sensors_list if s['unit'] == SENSOR_TEMP_UNIT]
elif sensor_type == SENSOR_FAN_TYPE:
ret = [s for s in self.sensors_list if s['unit'] == SENSOR_FAN_UNIT]
else:
# Unknown type
logger.debug("Unknown sensor type %s" % sensor_type)
ret = []
return ret

View File

@ -13,18 +13,20 @@ import psutil
from glances.logger import logger from glances.logger import logger
from glances.plugins.plugin.model import GlancesPluginModel from glances.plugins.plugin.model import GlancesPluginModel
from glances.globals import LINUX
# Batinfo library (optional; Linux-only) # Batinfo library (optional; Linux-only)
batinfo_tag = True if LINUX:
try: batinfo_tag = True
import batinfo try:
except ImportError: import batinfo
logger.debug("batinfo library not found. Fallback to psutil.") except ImportError:
logger.debug("batinfo library not found. Fallback to psutil.")
batinfo_tag = False
else:
batinfo_tag = False batinfo_tag = False
# Availability: # PsUtil Sensors_battery available on Linux, Windows, FreeBSD, macOS
# Linux, Windows, FreeBSD (psutil>=5.1.0)
# macOS (psutil>=5.4.2)
psutil_tag = True psutil_tag = True
try: try:
psutil.sensors_battery() psutil.sensors_battery()

View File

@ -22,6 +22,7 @@ podman; python_version >= "3.6"
potsdb potsdb
prometheus_client prometheus_client
pycouchdb pycouchdb
pydantic
pygal pygal
pymdstat pymdstat
pymongo; python_version >= "3.7" pymongo; python_version >= "3.7"

View File

@ -2,4 +2,3 @@ psutil>=5.6.7
defusedxml defusedxml
packaging packaging
ujson>=5.4.0 ujson>=5.4.0
pydantic