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

View File

@ -11,7 +11,7 @@
import time
from datetime import datetime
from pydantic import RootModel
from dataclasses import asdict
from glances.processes import glances_processes
from glances.thresholds import glances_thresholds
@ -201,7 +201,7 @@ class GlancesEventsList(object):
def get(self):
"""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):
"""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.client import InfluxDBClientError
FIELD_TO_TAG = ['name', 'cmdline']
FIELD_TO_TAG = ['name', 'cmdline', 'type']
class Export(GlancesExport):

View File

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

View File

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

View File

@ -16,8 +16,12 @@ from io import open
import webbrowser
from urllib.parse import urljoin
# Replace typing_extensions by typing when Python 3.8 support will be dropped
from typing import Annotated
try:
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.password import GlancesPassword

View File

@ -8,30 +8,34 @@
#
"""Sensors plugin."""
from enum import Enum
from concurrent.futures import ThreadPoolExecutor
from typing import List, Dict, Literal, Any
import psutil
import warnings
import threading
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.plugins.sensors.sensor.glances_batpercent import PluginModel as BatPercentPluginModel
from glances.plugins.sensors.sensor.glances_hddtemp import PluginModel as HddTempPluginModel
from glances.outputs.glances_unicode import unicode_message
from glances.plugins.plugin.model import GlancesPluginModel
SENSOR_TEMP_TYPE = 'temperature_core'
SENSOR_TEMP_UNIT = 'C'
SENSOR_FAN_TYPE = 'fan_speed'
SENSOR_FAN_UNIT = 'R'
class SensorType(str, Enum):
# 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'
SENSORS_BATTERY_UNIT = '%'
CPU_TEMP_UNIT = 'C'
FAN_SPEED_UNIT = 'R'
HDD_TEMP_UNIT = 'C'
BATTERY_UNIT = '%'
# Define the default refresh multiplicator
# Default value is 3 * Glances refresh time
@ -82,28 +86,38 @@ class PluginModel(GlancesPluginModel):
super(PluginModel, self).__init__(
args=args, config=config, stats_init_value=[], fields_description=fields_description
)
start_duration = Counter()
# Init the sensor class
start_duration.reset()
# Hotfix! Refactor to use only one `GlancesGrabSensors` later
self.glances_grab_sensors_fan_speed = GlancesGrabSensors()
self.glances_grab_sensors_temperature = GlancesGrabSensors()
logger.debug("Generic sensor plugin init duration: {} seconds".format(start_duration.get()))
glances_grab_sensors_cpu_temp = GlancesGrabSensors(SensorType.CPU_TEMP)
logger.debug("CPU Temp 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()
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()))
# Instance for the BatPercent in order to display the batteries
# capacities
# Instance for the BatPercent in order to display the batteries capacities
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()))
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
self.display_curse = True
@ -115,37 +129,15 @@ class PluginModel(GlancesPluginModel):
"""Return the key of the list."""
return 'label'
def __get_temperature(self, stats, index):
def __get_sensor_data(self, sensor_type: SensorType) -> List[Dict]:
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:
logger.error("Cannot grab sensors temperatures (%s)" % e)
logger.error(f"Cannot grab sensors `{sensor_type}` ({e})")
return []
else:
stats[index] = self.__transform_sensors(temperature)
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)
return self.__transform_sensors(data)
def __transform_sensors(self, threads_stats):
"""Hide, alias and sort the result"""
@ -172,22 +164,17 @@ class PluginModel(GlancesPluginModel):
stats = self.get_init_value()
if self.input_method == 'local':
threads_stats = [None] * 4
threads = [
threading.Thread(name=SENSOR_TEMP_TYPE, target=self.__get_temperature, args=(threads_stats, 0)),
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()
with ThreadPoolExecutor(max_workers=len(self.sensors_grab_map)) as executor:
logger.debug(f"Sensors enabled sub plugins: {list(self.sensors_grab_map.keys())}")
futures = {t: executor.submit(self.__get_sensor_data, t) for t in self.sensors_grab_map.keys()}
# Merge the results
for s in threads_stats:
stats.extend(s)
for sensor_type, future in futures.items():
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':
# Update stats using SNMP
# No standard:
@ -209,7 +196,7 @@ class PluginModel(GlancesPluginModel):
else:
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.
4 types of stats is possible in the sensors plugin:
@ -237,10 +224,10 @@ class PluginModel(GlancesPluginModel):
if not i['value']:
continue
# Alert processing
if i['type'] == SENSOR_TEMP_TYPE:
if self.is_limit('critical', stat_name=SENSOR_TEMP_TYPE + '_' + i['label']):
if i['type'] == SensorType.CPU_TEMP:
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)
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 use the system thresholds
if i['critical'] is None:
@ -253,7 +240,7 @@ class PluginModel(GlancesPluginModel):
alert = 'WARNING'
else:
alert = 'OK'
elif i['type'] == SENSORS_BATTERY_TYPE:
elif i['type'] == SensorType.BATTERY:
# Battery is in %
alert = self.get_alert(current=100 - i['value'], header=i['type'])
else:
@ -297,7 +284,7 @@ class PluginModel(GlancesPluginModel):
# Stats
for i in self.stats:
# 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
# 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'))
)
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 = ''
value = to_fahrenheit(i['value'])
unit = 'F'
@ -334,73 +321,42 @@ class PluginModel(GlancesPluginModel):
class GlancesGrabSensors(object):
"""Get sensors stats."""
def __init__(self):
def __init__(self, sensor_type: Literal[SensorType.FAN_SPEED, SensorType.CPU_TEMP]):
"""Init sensors stats."""
# Temperatures
self.init_temp = False
self.sensor_temps = {}
self.sensor_type = sensor_type
self.sensor_unit = CPU_TEMP_UNIT if self.sensor_type == SensorType.CPU_TEMP else FAN_SPEED_UNIT
self.init = False
try:
# psutil>=5.1.0, Linux-only
self.sensor_temps = psutil.sensors_temperatures()
self.__fetch_psutil()
self.init = True
except AttributeError:
logger.debug("Cannot grab temperatures. Platform not supported.")
else:
self.init_temp = True
logger.debug(f"Cannot grab {sensor_type}. Platform not supported.")
def __fetch_psutil(self) -> Dict[str, list]:
if self.sensor_type == SensorType.CPU_TEMP:
# Solve an issue #1203 concerning a RunTimeError warning message displayed
# in the curses interface.
warnings.filterwarnings("ignore")
# Fans
self.init_fan = False
self.sensor_fans = {}
try:
# psutil>=5.1.0, Linux-only
return psutil.sensors_temperatures()
if self.sensor_type == SensorType.FAN_SPEED:
# psutil>=5.2.0, Linux-only
self.sensor_fans = psutil.sensors_fans()
except AttributeError:
logger.debug("Cannot grab fans speed. Platform not supported.")
else:
self.init_fan = True
return psutil.sensors_fans()
# Init the stats
self.reset()
raise ValueError(f"Unsupported sensor_type: {self.sensor_type}")
def reset(self):
"""Reset/init the stats."""
self.sensors_list = []
def __update__(self):
def update(self) -> List[dict]:
"""Update the stats."""
# Reset the list
self.reset()
if not self.init_temp:
return self.sensors_list
if not self.init:
return []
# 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 = []
if type == SENSOR_TEMP_UNIT and self.init_temp:
input_list = self.sensor_temps
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):
data = self.__fetch_psutil()
for chip_name, chip in data.items():
label_index = 1
for chip_name_index, feature in enumerate(chip):
sensors_current = {}
@ -413,8 +369,9 @@ class GlancesGrabSensors(object):
else:
sensors_current['label'] = feature.label
# Sensors value, limit and unit
sensors_current['unit'] = type
sensors_current['value'] = int(getattr(feature, 'current', 0) if getattr(feature, 'current', 0) else 0)
sensors_current['unit'] = self.sensor_unit
sensors_current['value'] = int(
getattr(feature, 'current', 0) if getattr(feature, 'current', 0) else 0)
system_warning = getattr(feature, 'high', None)
system_critical = getattr(feature, 'critical', 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
ret.append(sensors_current)
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.plugins.plugin.model import GlancesPluginModel
from glances.globals import LINUX
# Batinfo library (optional; Linux-only)
batinfo_tag = True
try:
import batinfo
except ImportError:
logger.debug("batinfo library not found. Fallback to psutil.")
if LINUX:
batinfo_tag = True
try:
import batinfo
except ImportError:
logger.debug("batinfo library not found. Fallback to psutil.")
batinfo_tag = False
else:
batinfo_tag = False
# Availability:
# Linux, Windows, FreeBSD (psutil>=5.1.0)
# macOS (psutil>=5.4.2)
# PsUtil Sensors_battery available on Linux, Windows, FreeBSD, macOS
psutil_tag = True
try:
psutil.sensors_battery()

View File

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

View File

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