diff options
author | nicolargo <nicolas@nicolargo.com> | 2023-12-21 15:53:27 +0100 |
---|---|---|
committer | nicolargo <nicolas@nicolargo.com> | 2023-12-21 15:53:27 +0100 |
commit | 70250981b9ebabd3212c9e3d01c1fced49e1a33e (patch) | |
tree | 09569f96e0a7c0dfa2cd851e597f96379964e8a4 /glances/plugins/processlist/__init__.py | |
parent | e936a38dde886f151d75977b4ec947a8f5da2259 (diff) | |
parent | 380888a5648d914439f9b221dfd5ce589b2ffd54 (diff) |
Merge branch 'develop' into issue2183issue2183
Diffstat (limited to 'glances/plugins/processlist/__init__.py')
-rw-r--r-- | glances/plugins/processlist/__init__.py | 834 |
1 files changed, 834 insertions, 0 deletions
diff --git a/glances/plugins/processlist/__init__.py b/glances/plugins/processlist/__init__.py index e69de29b..6ec05133 100644 --- a/glances/plugins/processlist/__init__.py +++ b/glances/plugins/processlist/__init__.py @@ -0,0 +1,834 @@ +# -*- coding: utf-8 -*- +# +# This file is part of Glances. +# +# SPDX-FileCopyrightText: 2023 Nicolas Hennion <nicolas@nicolargo.com> +# +# SPDX-License-Identifier: LGPL-3.0-only +# + +"""Process list plugin.""" + +import os +import copy + +from glances.logger import logger +from glances.globals import WINDOWS, key_exist_value_not_none_not_v +from glances.processes import glances_processes, sort_stats +from glances.outputs.glances_unicode import unicode_message +from glances.plugins.core import PluginModel as CorePluginModel +from glances.plugins.plugin.model import GlancesPluginModel + + +def seconds_to_hms(input_seconds): + """Convert seconds to human-readable time.""" + minutes, seconds = divmod(input_seconds, 60) + hours, minutes = divmod(minutes, 60) + + hours = int(hours) + minutes = int(minutes) + seconds = str(int(seconds)).zfill(2) + + return hours, minutes, seconds + + +def split_cmdline(bare_process_name, cmdline): + """Return path, cmd and arguments for a process cmdline based on bare_process_name. + + If first argument of cmdline starts with the bare_process_name then + cmdline will just be considered cmd and path will be empty (see https://github.com/nicolargo/glances/issues/1795) + + :param bare_process_name: Name of the process from psutil + :param cmdline: cmdline from psutil + :return: Tuple with three strings, which are path, cmd and arguments of the process + """ + if cmdline[0].startswith(bare_process_name): + path, cmd = "", cmdline[0] + else: + path, cmd = os.path.split(cmdline[0]) + arguments = ' '.join(cmdline[1:]) + return path, cmd, arguments + + +class PluginModel(GlancesPluginModel): + """Glances' processes plugin. + + stats is a list + """ + + # Define the header layout of the processes list columns + layout_header = { + 'cpu': '{:<6} ', + 'mem': '{:<5} ', + 'virt': '{:<5} ', + 'res': '{:<5} ', + 'pid': '{:>{width}} ', + 'user': '{:<10} ', + 'time': '{:>8} ', + 'thread': '{:<3} ', + 'nice': '{:>3} ', + 'status': '{:>1} ', + 'ior': '{:>4} ', + 'iow': '{:<4} ', + 'command': '{} {}', + } + + # Define the stat layout of the processes list columns + layout_stat = { + 'cpu': '{:<6.1f}', + 'cpu_no_digit': '{:<6.0f}', + 'mem': '{:<5.1f} ', + 'virt': '{:<5} ', + 'res': '{:<5} ', + 'pid': '{:>{width}} ', + 'user': '{:<10} ', + 'time': '{:>8} ', + 'thread': '{:<3} ', + 'nice': '{:>3} ', + 'status': '{:>1} ', + 'ior': '{:>4} ', + 'iow': '{:<4} ', + 'command': '{}', + 'name': '[{}]', + } + + def __init__(self, args=None, config=None): + """Init the plugin.""" + super(PluginModel, self).__init__(args=args, config=config, stats_init_value=[]) + + # We want to display the stat in the curse interface + self.display_curse = True + + # Trying to display proc time + self.tag_proc_time = True + + # Call CorePluginModel to get the core number (needed when not in IRIX mode / Solaris mode) + try: + self.nb_log_core = CorePluginModel(args=self.args).update()["log"] + except Exception: + self.nb_log_core = 0 + + # Get the max values (dict) + self.max_values = copy.deepcopy(glances_processes.max_values()) + + # Get the maximum PID number + # 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: + if 'processlist' in config.as_dict() and '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) + + # The default sort key could also be overwrite by command line (see #1903) + if args.sort_processes_key is not None: + glances_processes.set_sort_key(args.sort_processes_key, False) + + # Note: 'glances_processes' is already init in the processes.py script + + def get_key(self): + """Return the key of the list.""" + return 'pid' + + def update(self): + """Update processes stats using the input method.""" + # Init new stats + stats = self.get_init_value() + + if self.input_method == 'local': + # Update stats using the standard system lib + # Note: Update is done in the processcount plugin + # Just return the processes list + if self.args.programs: + stats = glances_processes.getlist(as_programs=True) + else: + stats = glances_processes.getlist() + + elif self.input_method == 'snmp': + # No SNMP grab for processes + pass + + # Update the stats and transform all namedtuples to dict + self.stats = stats + + # Get the max values (dict) + # Use Deep copy to avoid change between update and display + self.max_values = copy.deepcopy(glances_processes.max_values()) + + return self.stats + + def get_nice_alert(self, value): + """Return the alert relative to the Nice configuration list""" + value = str(value) + try: + if value in self.get_limit('nice_critical'): + return 'CRITICAL' + except KeyError: + pass + try: + if value in self.get_limit('nice_warning'): + return 'WARNING' + except KeyError: + pass + try: + if value in self.get_limit('nice_careful'): + return 'CAREFUL' + except KeyError: + pass + return 'DEFAULT' + + def _get_process_curses_cpu(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'] + if args.disable_irix and self.nb_log_core != 0: + msg = cpu_layout.format(p['cpu_percent'] / float(self.nb_log_core)) + else: + msg = cpu_layout.format(p['cpu_percent']) + alert = self.get_alert( + p['cpu_percent'], + highlight_zero=False, + is_max=(p['cpu_percent'] == self.max_values['cpu_percent']), + header="cpu", + ) + ret = self.curse_add_line(msg, alert) + else: + msg = self.layout_header['cpu'].format('?') + ret = self.curse_add_line(msg) + return ret + + def _get_process_curses_mem(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']) + alert = self.get_alert( + p['memory_percent'], + highlight_zero=False, + is_max=(p['memory_percent'] == self.max_values['memory_percent']), + header="mem", + ) + ret = self.curse_add_line(msg, alert) + else: + msg = self.layout_header['mem'].format('?') + ret = self.curse_add_line(msg) + return ret + + def _get_process_curses_vms(self, p, selected, args): + """Return process VMS curses""" + if key_exist_value_not_none_not_v('memory_info', p, '', 1) and 'vms' in p['memory_info']: + msg = self.layout_stat['virt'].format(self.auto_unit(p['memory_info']['vms'], low_precision=False)) + ret = self.curse_add_line(msg, optional=True) + else: + msg = self.layout_header['virt'].format('?') + ret = self.curse_add_line(msg) + return ret + + def _get_process_curses_rss(self, p, selected, args): + """Return process RSS curses""" + if key_exist_value_not_none_not_v('memory_info', p, '', 0) and 'rss' in p['memory_info']: + msg = self.layout_stat['res'].format(self.auto_unit(p['memory_info']['rss'], low_precision=False)) + ret = self.curse_add_line(msg, optional=True) + else: + msg = self.layout_header['res'].format('?') + ret = self.curse_add_line(msg) + return ret + + def _get_process_curses_username(self, p, selected, args): + """Return process username curses""" + if 'username' in p: + # docker internal users are displayed as ints only, therefore str() + # Correct issue #886 on Windows OS + msg = self.layout_stat['user'].format(str(p['username'])[:9]) + ret = self.curse_add_line(msg) + else: + msg = self.layout_header['user'].format('?') + ret = self.curse_add_line(msg) + return ret + + def _get_process_curses_time(self, p, selected, args): + """Return process time curses""" + try: + # Sum user and system time + user_system_time = p['cpu_times']['user'] + p['cpu_times']['system'] + except (OverflowError, TypeError): + # Catch OverflowError on some Amazon EC2 server + # See https://github.com/nicolargo/glances/issues/87 + # Also catch TypeError on macOS + # See: https://github.com/nicolargo/glances/issues/622 + # logger.debug("Cannot get TIME+ ({})".format(e)) + msg = self.layout_header['time'].format('?') + ret = self.curse_add_line(msg, optional=True) + else: + hours, minutes, seconds = seconds_to_hms(user_system_time) + if hours > 99: + msg = '{:<7}h'.format(hours) + elif 0 < hours < 100: + msg = '{}h{}:{}'.format(hours, minutes, seconds) + else: + msg = '{}:{}'.format(minutes, seconds) + msg = self.layout_stat['time'].format(msg) + if hours > 0: + ret = self.curse_add_line(msg, decoration='CPU_TIME', optional=True) + else: + ret = self.curse_add_line(msg, optional=True) + return ret + + def _get_process_curses_thread(self, p, selected, args): + """Return process thread curses""" + if 'num_threads' in p: + num_threads = p['num_threads'] + if num_threads is None: + num_threads = '?' + msg = self.layout_stat['thread'].format(num_threads) + ret = self.curse_add_line(msg) + else: + msg = self.layout_header['thread'].format('?') + ret = self.curse_add_line(msg) + return ret + + def _get_process_curses_nice(self, p, selected, args): + """Return process nice curses""" + if 'nice' in p: + nice = p['nice'] + if nice is None: + nice = '?' + msg = self.layout_stat['nice'].format(nice) + ret = self.curse_add_line(msg, decoration=self.get_nice_alert(nice)) + else: + msg = self.layout_header['nice'].format('?') + ret = self.curse_add_line(msg) + return ret + + def _get_process_curses_status(self, p, selected, args): + """Return process status curses""" + if 'status' in p: + status = p['status'] + msg = self.layout_stat['status'].format(status) + if status == 'R': + ret = self.curse_add_line(msg, decoration='STATUS') + else: + ret = self.curse_add_line(msg) + else: + msg = self.layout_header['status'].format('?') + ret = self.curse_add_line(msg) + return ret + + def _get_process_curses_io(self, p, selected, args, rorw='ior'): + """Return process IO Read or Write curses""" + if 'io_counters' in p and p['io_counters'][4] == 1 and p['time_since_update'] != 0: + # Display rate if stats is available and io_tag ([4]) == 1 + # IO + io = int( + (p['io_counters'][0 if rorw == 'ior' else 1] - p['io_counters'][2 if rorw == 'ior' else 3]) + / p['time_since_update'] + ) + if io == 0: + msg = self.layout_stat[rorw].format("0") + else: + msg = self.layout_stat[rorw].format(self.auto_unit(io, low_precision=True)) + ret = self.curse_add_line(msg, optional=True, additional=True) + else: + msg = self.layout_header[rorw].format("?") + ret = self.curse_add_line(msg, optional=True, additional=True) + return ret + + def _get_process_curses_io_read(self, p, selected, args): + """Return process IO Read curses""" + return self._get_process_curses_io(p, selected, args, rorw='ior') + + def _get_process_curses_io_write(self, p, selected, args): + """Return process IO Write curses""" + return self._get_process_curses_io(p, selected, args, rorw='iow') + + def get_process_curses_data(self, p, selected, args): + """Get curses data to display for a process. + + - p is the process to display + - selected is a tag=True if p is the selected process + """ + ret = [self.curse_new_line()] + + # When a process is selected: + # * display a special character at the beginning of the line + # * underline the command name + ret.append( + self.curse_add_line( + unicode_message('PROCESS_SELECTOR') if (selected and not args.disable_cursor) else ' ', 'SELECTED' + ) + ) + + # CPU + ret.append(self._get_process_curses_cpu(p, selected, args)) + + # MEM + ret.append(self._get_process_curses_mem(p, selected, args)) + ret.append(self._get_process_curses_vms(p, selected, args)) + ret.append(self._get_process_curses_rss(p, selected, args)) + + # PID + if not self.args.programs: + # Display processes, so the PID should be displayed + msg = self.layout_stat['pid'].format(p['pid'], width=self.__max_pid_size()) + else: + # Display programs, so the PID should not be displayed + # Instead displays the number of children + msg = self.layout_stat['pid'].format( + len(p['childrens']) if 'childrens' in p else '_', width=self.__max_pid_size() + ) + ret.append(self.curse_add_line(msg)) + + # USER + ret.append(self._get_process_curses_username(p, selected, args)) + + # TIME+ + ret.append(self._get_process_curses_time(p, selected, args)) + + # THREAD + ret.append(self._get_process_curses_thread(p, selected, args)) + + # NICE + ret.append(self._get_process_curses_nice(p, selected, args)) + + # STATUS + ret.append(self._get_process_curses_status(p, selected, args)) + + # IO read/write + ret.append(self._get_process_curses_io_read(p, selected, args)) + ret.append(self._get_process_curses_io_write(p, selected, args)) + + # Command line + # If no command line for the process is available, fallback to the bare process name instead + bare_process_name = p['name'] + cmdline = p.get('cmdline', '?') + + try: + process_decoration = 'PROCESS_SELECTED' if (selected and not args.disable_cursor) else 'PROCESS' + if cmdline: + path, cmd, arguments = split_cmdline(bare_process_name, cmdline) + # Manage end of line in arguments (see #1692) + arguments = arguments.replace('\r\n', ' ') + arguments = arguments.replace('\n', ' ') + arguments = arguments.replace('\t', ' ') + if os.path.isdir(path) and not args.process_short_name: + msg = self.layout_stat['command'].format(path) + os.sep + ret.append(self.curse_add_line(msg, splittable=True)) + ret.append(self.curse_add_line(cmd, decoration=process_decoration, splittable=True)) + else: + msg = self.layout_stat['command'].format(cmd) + ret.append(self.curse_add_line(msg, decoration=process_decoration, splittable=True)) + if arguments: + msg = ' ' + self.layout_stat['command'].format(arguments) + ret.append(self.curse_add_line(msg, splittable=True)) + else: + msg = self.layout_stat['name'].format(bare_process_name) + ret.append(self.curse_add_line(msg, decoration=process_decoration, splittable=True)) + except (TypeError, UnicodeEncodeError) as e: + # Avoid crash after running fine for several hours #1335 + logger.debug("Can not decode command line '{}' ({})".format(cmdline, e)) + ret.append(self.curse_add_line('', splittable=True)) + + return ret + + def is_selected_process(self, args): + return ( + args.is_standalone + and self.args.enable_process_extended + and args.cursor_position is not None + and glances_processes.extended_process is not None + ) + + def msg_curse(self, args=None, max_width=None): + """Return the dict to display in the curse interface.""" + # Init the return message + ret = [] + + # Only process if stats exist and display plugin enable... + if not self.stats or args.disable_process: + return ret + + # Compute the sort key + process_sort_key = glances_processes.sort_key + processes_list_sorted = self.__sort_stats(process_sort_key) + + # Display extended stats for selected process + ############################################# + + if self.is_selected_process(args): + self.__msg_curse_extended_process(ret, glances_processes.extended_process) + + # Display others processes list + ############################### + + # Header + self.__msg_curse_header(ret, process_sort_key, args) + + # Process list + # Loop over processes (sorted by the sort key previously compute) + # This is a Glances bottleneck (see flame graph), + # TODO: get_process_curses_data should be optimzed + for position, process in enumerate(processes_list_sorted): + ret.extend(self.get_process_curses_data(process, position == args.cursor_position, args)) + + # A filter is set Display the stats summaries + if glances_processes.process_filter is not None: + if args.reset_minmax_tag: + args.reset_minmax_tag = not args.reset_minmax_tag + self.__mmm_reset() + self.__msg_curse_sum(ret, args=args) + self.__msg_curse_sum(ret, mmm='min', args=args) + self.__msg_curse_sum(ret, mmm='max', args=args) + + # Return the message with decoration + return ret + + def __msg_curse_extended_process(self, ret, p): + """Get extended curses data for the selected process (see issue #2225) + + The result depends of the process type (process or thread). + + Input p is a dict with the following keys: + {'status': 'S', + 'memory_info': {'rss': 466890752, 'vms': 3365347328, 'shared': 68153344, + 'text': 659456, 'lib': 0, 'data': 774647808, 'dirty': 0], + 'pid': 4980, + 'io_counters': [165385216, 0, 165385216, 0, 1], + 'num_threads': 20, + 'nice': 0, + 'memory_percent': 5.958135664449709, + 'cpu_percent': 0.0, + 'gids': {'real': 1000, 'effective': 1000, 'saved': 1000}, + 'cpu_times': {'user': 696.38, 'system': 119.98, 'children_user': 0.0, + 'children_system': 0.0, 'iowait': 0.0), + 'name': 'WebExtensions', + 'key': 'pid', + 'time_since_update': 2.1997854709625244, + 'cmdline': ['/snap/firefox/2154/usr/lib/firefox/firefox', '-contentproc', '-childID', '...'], + 'username': 'nicolargo', + 'cpu_min': 0.0, + 'cpu_max': 7.0, + 'cpu_mean': 3.2} + """ + if self.args.programs: + self.__msg_curse_extended_process_program(ret, p) + else: + self.__msg_curse_extended_process_thread(ret, p) + + def __msg_curse_extended_process_program(self, ret, p): + # Title + msg = "Pinned program {} ('e' to unpin)".format(p['name']) + ret.append(self.curse_add_line(msg, "TITLE")) + + ret.append(self.curse_new_line()) + ret.append(self.curse_new_line()) + + def __msg_curse_extended_process_thread(self, ret, p): + # Title + ret.append(self.curse_add_line("Pinned thread ", "TITLE")) + ret.append(self.curse_add_line(p['name'], "UNDERLINE")) + ret.append(self.curse_add_line(" ('e' to unpin)")) + + # First line is CPU affinity + ret.append(self.curse_new_line()) + ret.append(self.curse_add_line(' CPU Min/Max/Mean: ')) + msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(p['cpu_min'], p['cpu_max'], p['cpu_mean']) + ret.append(self.curse_add_line(msg, decoration='INFO')) + if 'cpu_affinity' in p and p['cpu_affinity'] is not None: + ret.append(self.curse_add_line(' Affinity: ')) + ret.append(self.curse_add_line(str(len(p['cpu_affinity'])), decoration='INFO')) + ret.append(self.curse_add_line(' cores', decoration='INFO')) + if 'ionice' in p and p['ionice'] is not None and hasattr(p['ionice'], 'ioclass'): + msg = ' IO nice: ' + k = 'Class is ' + v = p['ionice'].ioclass + # Linux: The scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle. + # Windows: On Windows only ioclass is used and it can be set to 2 (normal), 1 (low) or 0 (very low). + if WINDOWS: + if v == 0: + msg += k + 'Very Low' + elif v == 1: + msg += k + 'Low' + elif v == 2: + msg += 'No specific I/O priority' + else: + msg += k + str(v) + else: + if v == 0: + msg += 'No specific I/O priority' + elif v == 1: + msg += k + 'Real Time' + elif v == 2: + msg += k + 'Best Effort' + elif v == 3: + msg += k + 'IDLE' + else: + msg += k + str(v) + # value is a number which goes from 0 to 7. + # The higher the value, the lower the I/O priority of the process. + if hasattr(p['ionice'], 'value') and p['ionice'].value != 0: + msg += ' (value %s/7)' % str(p['ionice'].value) + ret.append(self.curse_add_line(msg, splittable=True)) + + # Second line is memory info + ret.append(self.curse_new_line()) + ret.append(self.curse_add_line(' MEM Min/Max/Mean: ')) + msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(p['memory_min'], p['memory_max'], p['memory_mean']) + ret.append(self.curse_add_line(msg, decoration='INFO')) + if 'memory_info' in p and p['memory_info'] is not None: + ret.append(self.curse_add_line(' Memory info: ')) + for k, v in p['memory_info'].items(): + ret.append( + self.curse_add_line( + self.auto_unit(v, low_precision=False), + decoration='INFO', + splittable=True, + ) + ) + ret.append(self.curse_add_line(' ' + k + ' ', splittable=True)) + if 'memory_swap' in p and p['memory_swap'] is not None: + ret.append( + self.curse_add_line( + self.auto_unit(p['memory_swap'], low_precision=False), decoration='INFO', splittable=True + ) + ) + ret.append(self.curse_add_line(' swap ', splittable=True)) + + # Third line is for open files/network sessions + ret.append(self.curse_new_line()) + ret.append(self.curse_add_line(' Open: ')) + for stat_prefix in ['num_threads', 'num_fds', 'num_handles', 'tcp', 'udp']: + if stat_prefix in p and p[stat_prefix] is not None: + ret.append(self.curse_add_line(str(p[stat_prefix]), decoration='INFO')) + ret.append(self.curse_add_line(' {} '.format(stat_prefix.replace('num_', '')))) + + ret.append(self.curse_new_line()) + ret.append(self.curse_new_line()) + + def __msg_curse_header(self, ret, process_sort_key, args=None): + """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 + ) + ) + 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)" + else: + 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')) + + def __msg_curse_sum(self, ret, sep_char='_', mmm=None, args=None): + """ + Build the sum message (only when filter is on) and add it to the ret dict. + + :param ret: list of string where the message is added + :param sep_char: define the line separation char + :param mmm: display min, max, mean or current (if mmm=None) + :param args: Glances args + """ + ret.append(self.curse_new_line()) + if mmm is None: + ret.append(self.curse_add_line(sep_char * 69)) + ret.append(self.curse_new_line()) + # CPU percent sum + msg = self.layout_stat['cpu'].format(self.__sum_stats('cpu_percent', mmm=mmm)) + ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm))) + # MEM percent sum + msg = self.layout_stat['mem'].format(self.__sum_stats('memory_percent', mmm=mmm)) + ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm))) + # VIRT and RES memory sum + if ( + 'memory_info' in self.stats[0] + and self.stats[0]['memory_info'] is not None + and self.stats[0]['memory_info'] != '' + ): + # VMS + msg = self.layout_stat['virt'].format( + self.auto_unit(self.__sum_stats('memory_info', sub_key='vms', mmm=mmm), low_precision=False) + ) + ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm), optional=True)) + # RSS + msg = self.layout_stat['res'].format( + self.auto_unit(self.__sum_stats('memory_info', sub_key='rss', mmm=mmm), low_precision=False) + ) + ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm), optional=True)) + else: + msg = self.layout_header['virt'].format('') + ret.append(self.curse_add_line(msg)) + msg = self.layout_header['res'].format('') + ret.append(self.curse_add_line(msg)) + # PID + msg = self.layout_header['pid'].format('', width=self.__max_pid_size()) + ret.append(self.curse_add_line(msg)) + # USER + msg = self.layout_header['user'].format('') + ret.append(self.curse_add_line(msg)) + # TIME+ + msg = self.layout_header['time'].format('') + ret.append(self.curse_add_line(msg, optional=True)) + # THREAD + msg = self.layout_header['thread'].format('') + ret.append(self.curse_add_line(msg)) + # NICE + msg = self.layout_header['nice'].format('') + ret.append(self.curse_add_line(msg)) + # STATUS + msg = self.layout_header['status'].format('') + ret.append(self.curse_add_line(msg)) + # IO read/write + if 'io_counters' in self.stats[0] and mmm is None: + # IO read + io_rs = int( + (self.__sum_stats('io_counters', 0) - self.__sum_stats('io_counters', sub_key=2, mmm=mmm)) + / self.stats[0]['time_since_update'] + ) + if io_rs == 0: + msg = self.layout_stat['ior'].format('0') + else: + msg = self.layout_stat['ior'].format(self.auto_unit(io_rs, low_precision=True)) + ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm), optional=True, additional=True)) + # IO write + io_ws = int( + (self.__sum_stats('io_counters', 1) - self.__sum_stats('io_counters', sub_key=3, mmm=mmm)) + / self.stats[0]['time_since_update'] + ) + if io_ws == 0: + msg = self.layout_stat['iow'].format('0') + else: + msg = self.layout_stat['iow'].format(self.auto_unit(io_ws, low_precision=True)) + ret.append(self.curse_add_line(msg, decoration=self.__mmm_deco(mmm), optional=True, additional=True)) + else: + msg = self.layout_header['ior'].format('') + ret.append(self.curse_add_line(msg, optional=True, additional=True)) + msg = self.layout_header['iow'].format('') + ret.append(self.curse_add_line(msg, optional=True, additional=True)) + if mmm is None: + msg = ' < {}'.format('current') + ret.append(self.curse_add_line(msg, optional=True)) + else: + msg = ' < {}'.format(mmm) + ret.append(self.curse_add_line(msg, optional=True)) + msg = ' (\'M\' to reset)' + ret.append(self.curse_add_line(msg, optional=True)) + + def __mmm_deco(self, mmm): + """Return the decoration string for the current mmm status.""" + if mmm is not None: + return 'DEFAULT' + else: + return 'FILTER' + + def __mmm_reset(self): + """Reset the MMM stats.""" + self.mmm_min = {} + self.mmm_max = {} + + def __sum_stats(self, key, sub_key=None, mmm=None): + """Return the sum of the stats value for the given key. + + :param sub_key: If sub_key is set, get the p[key][sub_key] + :param mmm: display min, max, mean or current (if mmm=None) + """ + # Compute stats summary + ret = 0 + for p in self.stats: + if key not in p: + # Correct issue #1188 + continue + if p[key] is None: + # Correct https://github.com/nicolargo/glances/issues/1105#issuecomment-363553788 + continue + if sub_key is None: + ret += p[key] + else: + ret += p[key][sub_key] + + # Manage Min/Max/Mean + mmm_key = self.__mmm_key(key, sub_key) + if mmm == 'min': + try: + if self.mmm_min[mmm_key] > ret: + self.mmm_min[mmm_key] = ret + except AttributeError: + self.mmm_min = {} + return 0 + except KeyError: + self.mmm_min[mmm_key] = ret + ret = self.mmm_min[mmm_key] + elif mmm == 'max': + try: + if self.mmm_max[mmm_key] < ret: + self.mmm_max[mmm_key] = ret + except AttributeError: + self.mmm_max = {} + return 0 + except KeyError: + self.mmm_max[mmm_key] = ret + ret = self.mmm_max[mmm_key] + + return ret + + def __mmm_key(self, key, sub_key): + ret = key + if sub_key is not None: + ret += str(sub_key) + return ret + + def __sort_stats(self, sorted_by=None): + """Return the stats (dict) sorted by (sorted_by).""" + return sort_stats(self.stats, sorted_by, reverse=glances_processes.sort_reverse) + + def __max_pid_size(self): + """Return the maximum PID size in number of char.""" + if self.pid_max is not None: + return len(str(self.pid_max)) + else: + # By default return 5 (corresponding to 99999 PID number) + return 5 |