From 34520ca45b10762e678947bba5663e6d7484fc13 Mon Sep 17 00:00:00 2001 From: nicolargo Date: Sun, 10 Nov 2024 20:00:40 +0100 Subject: [PATCH] First version but only for the TUI standalone mode --- conf/glances.conf | 4 + glances/main.py | 6 +- glances/outputs/glances_curses.py | 4 +- glances/plugins/processlist/__init__.py | 185 ++++++++++++++---------- glances/processes.py | 34 ++++- glances/server.py | 4 + glances/standalone.py | 2 +- glances/webserver.py | 3 + 8 files changed, 153 insertions(+), 89 deletions(-) diff --git a/conf/glances.conf b/conf/glances.conf index a57756e9..a038e4ee 100644 --- a/conf/glances.conf +++ b/conf/glances.conf @@ -410,6 +410,10 @@ disable=False # Should be one of the following: # cpu_percent, memory_percent, io_counters, name, cpu_times, username #sort_key=memory_percent +# List of stats to disable (not grabed and not display) +# Stats that can be disabled: cpu_percent,memory_info,memory_percent,username,cpu_times,num_threads,nice,status,io_counters,cmdline +# Stats that can not be disable: pid,name +#disable_stats=cpu_percent,memory_info,memory_percent,username,cpu_times,num_threads,nice,status,io_counters,cmdline # Define CPU/MEM (per process) thresholds in % # Default values if not defined: 50/70/90 cpu_careful=50 diff --git a/glances/main.py b/glances/main.py index a9a485b0..53dda082 100644 --- a/glances/main.py +++ b/glances/main.py @@ -18,7 +18,7 @@ from glances import __apiversion__, __version__, psutil_version from glances.config import Config from glances.globals import WINDOWS, disable, enable from glances.logger import LOG_FILENAME, logger -from glances.processes import sort_processes_key_list +from glances.processes import sort_processes_stats_list class GlancesMain: @@ -269,8 +269,8 @@ Examples of use: parser.add_argument( '--sort-processes', dest='sort_processes_key', - choices=sort_processes_key_list, - help='Sort processes by: {}'.format(', '.join(sort_processes_key_list)), + choices=sort_processes_stats_list, + help='Sort processes by: {}'.format(', '.join(sort_processes_stats_list)), ) # Display processes list by program name and not by thread parser.add_argument( diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index 9dc8ab36..37590df8 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -16,7 +16,7 @@ from glances.globals import MACOS, WINDOWS, disable, enable, itervalues, natives from glances.logger import logger from glances.outputs.glances_colors import GlancesColors from glances.outputs.glances_unicode import unicode_message -from glances.processes import glances_processes, sort_processes_key_list +from glances.processes import glances_processes, sort_processes_stats_list from glances.timer import Timer # Import curses library for "normal" operating system @@ -97,7 +97,7 @@ class _GlancesCurses: # 'DOWN' > Down in the server list } - _sort_loop = sort_processes_key_list + _sort_loop = sort_processes_stats_list # Define top menu _top = ['quicklook', 'cpu', 'percpu', 'gpu', 'mem', 'memswap', 'load'] diff --git a/glances/plugins/processlist/__init__.py b/glances/plugins/processlist/__init__.py index 6927de28..1dcd8e49 100644 --- a/glances/plugins/processlist/__init__.py +++ b/glances/plugins/processlist/__init__.py @@ -117,6 +117,22 @@ class PluginModel(GlancesPluginModel): stats is a list """ + # Default list of processes stats to be grabbed / displayed + # Can be altered by glances_processes.disable_stats + enable_stats = [ + 'cpu_percent', + 'memory_percent', + 'memory_info', # vms and rss + 'pid', + 'username', + 'cpu_times', + 'num_threads', + 'nice', + 'status', + 'io_counters', # ior and iow + 'cmdline', + ] + # Define the header layout of the processes list columns layout_header = { 'cpu': '{:<6} ', @@ -176,19 +192,8 @@ class PluginModel(GlancesPluginModel): # Use to optimize space (see https://github.com/nicolargo/glances/issues/959) self.pid_max = glances_processes.pid_max - # Set the default sort key if it is defined in the configuration file - if config is not None and 'processlist' in config.as_dict(): - if 'sort_key' in config.as_dict()['processlist']: - logger.debug( - 'Configuration overwrites processes sort key by {}'.format( - config.as_dict()['processlist']['sort_key'] - ) - ) - glances_processes.set_sort_key(config.as_dict()['processlist']['sort_key'], False) - if 'export' in config.as_dict()['processlist']: - glances_processes.export_process_filter = config.as_dict()['processlist']['export'] - if args.export: - logger.info("Export process filter is set to: {}".format(config.as_dict()['processlist']['export'])) + # Load the config file + self.load(args, config) # The default sort key could also be overwrite by command line (see #1903) if args and args.sort_processes_key is not None: @@ -196,6 +201,27 @@ class PluginModel(GlancesPluginModel): # Note: 'glances_processes' is already init in the processes.py script + def load(self, args, config): + # Set the default sort key if it is defined in the configuration file + if config is None or 'processlist' not in config.as_dict(): + return + if 'sort_key' in config.as_dict()['processlist']: + logger.debug( + 'Configuration overwrites processes sort key by {}'.format(config.as_dict()['processlist']['sort_key']) + ) + glances_processes.set_sort_key(config.as_dict()['processlist']['sort_key'], False) + if 'export' in config.as_dict()['processlist']: + glances_processes.export_process_filter = config.as_dict()['processlist']['export'] + if args.export: + logger.info("Export process filter is set to: {}".format(config.as_dict()['processlist']['export'])) + if 'disable_stats' in config.as_dict()['processlist']: + logger.info( + 'Followings processes stats wil not be displayed: {}'.format( + config.as_dict()['processlist']['disable_stats'] + ) + ) + glances_processes.disable_stats = config.as_dict()['processlist']['disable_stats'].split(',') + def get_key(self): """Return the key of the list.""" return 'pid' @@ -255,7 +281,7 @@ class PluginModel(GlancesPluginModel): pass return 'DEFAULT' - def _get_process_curses_cpu(self, p, selected, args): + def _get_process_curses_cpu_percent(self, p, selected, args): """Return process CPU curses""" if key_exist_value_not_none_not_v('cpu_percent', p, ''): cpu_layout = self.layout_stat['cpu'] if p['cpu_percent'] < 100 else self.layout_stat['cpu_no_digit'] @@ -275,7 +301,7 @@ class PluginModel(GlancesPluginModel): ret = self.curse_add_line(msg) return ret - def _get_process_curses_mem(self, p, selected, args): + def _get_process_curses_memory_percent(self, p, selected, args): """Return process MEM curses""" if key_exist_value_not_none_not_v('memory_percent', p, ''): msg = self.layout_stat['mem'].format(p['memory_percent']) @@ -311,6 +337,12 @@ class PluginModel(GlancesPluginModel): ret = self.curse_add_line(msg) return ret + def _get_process_curses_memory_info(self, p, selected, args): + return [ + self._get_process_curses_vms(p, selected, args), + self._get_process_curses_rss(p, selected, args), + ] + def _get_process_curses_pid(self, p, selected, args): """Return process PID curses""" if not self.args.programs: @@ -334,7 +366,7 @@ class PluginModel(GlancesPluginModel): msg = self.layout_header['user'].format('?') return self.curse_add_line(msg) - def _get_process_curses_time(self, p, selected, args): + def _get_process_curses_cpu_times(self, p, selected, args): """Return process time curses""" cpu_times = p['cpu_times'] try: @@ -365,7 +397,7 @@ class PluginModel(GlancesPluginModel): return self.curse_add_line(msg, optional=True) - def _get_process_curses_thread(self, p, selected, args): + def _get_process_curses_num_threads(self, p, selected, args): """Return process thread curses""" if 'num_threads' in p: num_threads = p['num_threads'] @@ -422,14 +454,14 @@ class PluginModel(GlancesPluginModel): ret = self.curse_add_line(msg, optional=True, additional=True) return ret - def _get_process_curses_io(self, p, selected, args): + def _get_process_curses_io_counters(self, p, selected, args): return [ self._get_process_curses_io_read_write(p, selected, args, rorw='ior'), self._get_process_curses_io_read_write(p, selected, args, rorw='iow'), ] - def _get_process_curses_command(self, p, selected, args): - """Return process command curses""" + def _get_process_curses_cmdline(self, p, selected, args): + """Return process cmdline curses""" ret = [] # If no command line for the process is available, fallback to the bare process name instead bare_process_name = p['name'] @@ -476,20 +508,7 @@ class PluginModel(GlancesPluginModel): ) ) - for stat in [ - 'cpu', - 'mem', - 'vms', - 'rss', - 'pid', - 'username', - 'time', - 'thread', - 'nice', - 'status', - 'io', - 'command', - ]: + for stat in [i for i in self.enable_stats if i not in glances_processes.disable_stats]: msg = getattr(self, f'_get_process_curses_{stat}')(p, selected, args) if isinstance(msg, list): # ex: _get_process_curses_command return a list, so extend @@ -718,48 +737,61 @@ class PluginModel(GlancesPluginModel): """Build the header and add it to the ret dict.""" sort_style = 'SORT' - if args.disable_irix and 0 < self.nb_log_core < 10: - msg = self.layout_header['cpu'].format('CPU%/' + str(self.nb_log_core)) - elif args.disable_irix and self.nb_log_core != 0: - msg = self.layout_header['cpu'].format('CPU%/C') - else: - msg = self.layout_header['cpu'].format('CPU%') - ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_percent' else 'DEFAULT')) - msg = self.layout_header['mem'].format('MEM%') - ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'memory_percent' else 'DEFAULT')) - msg = self.layout_header['virt'].format('VIRT') - ret.append(self.curse_add_line(msg, optional=True)) - msg = self.layout_header['res'].format('RES') - ret.append(self.curse_add_line(msg, optional=True)) - if not self.args.programs: - msg = self.layout_header['pid'].format('PID', width=self.__max_pid_size()) - else: - msg = self.layout_header['pid'].format('NPROCS', width=self.__max_pid_size()) - ret.append(self.curse_add_line(msg)) - msg = self.layout_header['user'].format('USER') - ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'username' else 'DEFAULT')) - msg = self.layout_header['time'].format('TIME+') - ret.append( - self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_times' else 'DEFAULT', optional=True) - ) - msg = self.layout_header['thread'].format('THR') - ret.append(self.curse_add_line(msg)) - msg = self.layout_header['nice'].format('NI') - ret.append(self.curse_add_line(msg)) - msg = self.layout_header['status'].format('S') - ret.append(self.curse_add_line(msg)) - msg = self.layout_header['ior'].format('R/s') - ret.append( - self.curse_add_line( - msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True + display_stats = [i for i in self.enable_stats if i not in glances_processes.disable_stats] + + if 'cpu_percent' in display_stats: + if args.disable_irix and 0 < self.nb_log_core < 10: + msg = self.layout_header['cpu'].format('CPU%/' + str(self.nb_log_core)) + elif args.disable_irix and self.nb_log_core != 0: + msg = self.layout_header['cpu'].format('CPU%/C') + else: + msg = self.layout_header['cpu'].format('CPU%') + ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_percent' else 'DEFAULT')) + + if 'memory_percent' in display_stats: + msg = self.layout_header['mem'].format('MEM%') + ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'memory_percent' else 'DEFAULT')) + if 'memory_info' in display_stats: + msg = self.layout_header['virt'].format('VIRT') + ret.append(self.curse_add_line(msg, optional=True)) + msg = self.layout_header['res'].format('RES') + ret.append(self.curse_add_line(msg, optional=True)) + if 'pid' in display_stats: + if not self.args.programs: + msg = self.layout_header['pid'].format('PID', width=self.__max_pid_size()) + else: + msg = self.layout_header['pid'].format('NPROCS', width=self.__max_pid_size()) + ret.append(self.curse_add_line(msg)) + if 'username' in display_stats: + msg = self.layout_header['user'].format('USER') + ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'username' else 'DEFAULT')) + if 'cpu_times' in display_stats: + msg = self.layout_header['time'].format('TIME+') + ret.append( + self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_times' else 'DEFAULT', optional=True) ) - ) - msg = self.layout_header['iow'].format('W/s') - ret.append( - self.curse_add_line( - msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True + if 'num_threads' in display_stats: + msg = self.layout_header['thread'].format('THR') + ret.append(self.curse_add_line(msg)) + if 'nice' in display_stats: + msg = self.layout_header['nice'].format('NI') + ret.append(self.curse_add_line(msg)) + if 'status' in display_stats: + msg = self.layout_header['status'].format('S') + ret.append(self.curse_add_line(msg)) + if 'io_counters' in display_stats: + msg = self.layout_header['ior'].format('R/s') + ret.append( + self.curse_add_line( + msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True + ) + ) + msg = self.layout_header['iow'].format('W/s') + ret.append( + self.curse_add_line( + msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True + ) ) - ) if args.is_standalone and not args.disable_cursor: if self.args.programs: shortkey = "('k' to kill)" @@ -767,8 +799,9 @@ class PluginModel(GlancesPluginModel): shortkey = "('e' to pin | 'k' to kill)" else: shortkey = "" - msg = self.layout_header['command'].format("Programs" if self.args.programs else "Command", shortkey) - ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'name' else 'DEFAULT')) + if 'cmdline' in display_stats: + msg = self.layout_header['command'].format("Programs" if self.args.programs else "Command", shortkey) + ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'name' else 'DEFAULT')) def __msg_curse_sum(self, ret, sep_char='_', mmm=None, args=None): """ diff --git a/glances/processes.py b/glances/processes.py index 85214f81..81306cbe 100644 --- a/glances/processes.py +++ b/glances/processes.py @@ -18,8 +18,11 @@ from glances.timer import Timer, getTimeSinceLastUpdate psutil_version_info = tuple([int(num) for num in psutil.__version__.split('.')]) +# This constant defines the list of mandatory processes stats. Thoses stats can not be disabled by the user +mandatory_processes_stats_list = ['pid', 'name'] + # This constant defines the list of available processes sort key -sort_processes_key_list = ['cpu_percent', 'memory_percent', 'username', 'cpu_times', 'io_counters', 'name'] +sort_processes_stats_list = ['cpu_percent', 'memory_percent', 'username', 'cpu_times', 'io_counters', 'name'] # Sort dictionary for human sort_for_human = { @@ -38,7 +41,7 @@ class GlancesProcesses: def __init__(self, cache_timeout=60): """Init the class to collect stats about processes.""" - # Init the args, coming from the GlancesStandalone class + # Init the args, coming from the classes derived from GlancesMode # Should be set by the set_args method self.args = None @@ -96,6 +99,9 @@ class GlancesProcesses: self._max_values = {} self.reset_max_values() + # Set the key's list be disabled in order to only display specific attribte in the process list + self.disable_stats = [] + def _test_grab(self): """Test somes optionals features""" # Test if the system can grab io_counters @@ -143,9 +149,12 @@ class GlancesProcesses: # For each key in the processcount dict # count the number of processes with the same status for k in iterkeys(self.processcount): - self.processcount[k] = len(list(filter(lambda v: v['status'] is k, plist))) + self.processcount[k] = len(list(filter(lambda v: v.get('status', '?') is k, plist))) # Compute thread - self.processcount['thread'] = sum(i['num_threads'] for i in plist if i['num_threads'] is not None) + try: + self.processcount['thread'] = sum(i['num_threads'] for i in plist if i['num_threads'] is not None) + except KeyError: + self.processcount['thread'] = None # Compute total self.processcount['total'] = len(plist) @@ -215,7 +224,15 @@ class GlancesProcesses: """Set the maximum number of processes showed in the UI.""" self._max_processes = value - # Process filter + @property + def disable_stats(self): + """Set disable_stats list""" + return self._disable_stats + + @disable_stats.setter + def disable_stats(self, stats_list): + """Set disable_stats list""" + self._disable_stats = [i for i in stats_list if i not in mandatory_processes_stats_list] @property def process_filter_input(self): @@ -445,6 +462,9 @@ class GlancesProcesses: else: is_cached = True + # Remove attributes set by the user in the config file (see #1524) + sorted_attrs = [i for i in sorted_attrs if i not in self.disable_stats] + # Build the processes stats list (it is why we need psutil>=5.3.0) (see issue #2755) processlist = list( filter( @@ -491,7 +511,7 @@ class GlancesProcesses: proc['time_since_update'] = time_since_update # Process status (only keep the first char) - proc['status'] = str(proc['status'])[:1].upper() + proc['status'] = str(proc.get('status', '?'))[:1].upper() # Process IO # procstat['io_counters'] is a list: @@ -553,7 +573,7 @@ class GlancesProcesses: # Compute the maximum value for keys in self._max_values_list: CPU, MEM # Useful to highlight the processes with maximum values - for k in self._max_values_list: + for k in [i for i in self._max_values_list if i not in self.disable_stats]: values_list = [i[k] for i in processlist if i[k] is not None] if values_list: self.set_max_values(k, max(values_list)) diff --git a/glances/server.py b/glances/server.py index 785e0539..8946fa86 100644 --- a/glances/server.py +++ b/glances/server.py @@ -17,6 +17,7 @@ from defusedxml import xmlrpc from glances import __version__ from glances.logger import logger +from glances.processes import glances_processes from glances.servers_list_dynamic import GlancesAutoDiscoverClient from glances.stats_server import GlancesStatsServer from glances.timer import Timer @@ -176,6 +177,9 @@ class GlancesServer: # Args self.args = args + # Set the args for the glances_processes instance + glances_processes.set_args(args) + # Init the XML RPC server try: self.server = GlancesXMLRPCServer(args.bind_address, args.port, requestHandler, config=config) diff --git a/glances/standalone.py b/glances/standalone.py index 7786f539..f4e12ff0 100644 --- a/glances/standalone.py +++ b/glances/standalone.py @@ -48,7 +48,7 @@ class GlancesStandalone: self.display_modules_list() sys.exit(0) - # The args is needed to get the selected process in the process list (Curses mode) + # Set the args for the glances_processes instance glances_processes.set_args(args) # If process extended stats is disabled by user diff --git a/glances/webserver.py b/glances/webserver.py index f56e9256..8a241545 100644 --- a/glances/webserver.py +++ b/glances/webserver.py @@ -25,6 +25,9 @@ class GlancesWebServer: # Ignore kernel threads in process list glances_processes.disable_kernel_threads() + # Set the args for the glances_processes instance + glances_processes.set_args(args) + # Initial system information update self.stats.update()