From 9614e2bb19c6bdd512fea5dafbed1250da0049d9 Mon Sep 17 00:00:00 2001 From: nicolargo Date: Tue, 27 Dec 2022 10:45:51 +0100 Subject: [PATCH] First version but UI should be improved and when user is in program mode, it did not work... --- glances/main.py | 12 ++ glances/outputs/glances_curses.py | 6 +- glances/plugins/glances_processlist.py | 185 +++++++++++++++---------- glances/processes.py | 136 +++++++++++------- glances/standalone.py | 3 + 5 files changed, 216 insertions(+), 126 deletions(-) diff --git a/glances/main.py b/glances/main.py index aff37d4f..059655f6 100644 --- a/glances/main.py +++ b/glances/main.py @@ -236,6 +236,13 @@ Examples of use: dest='enable_separator', help='enable separator in the UI', ), + parser.add_argument( + '--disable-cursor', + action='store_true', + default=False, + dest='disable_cursor', + help='disable cursor (process selection) in the UI', + ), # Sort processes list parser.add_argument( '--sort-processes', @@ -700,6 +707,11 @@ Examples of use: logger.critical("Process filter is only available in standalone mode") sys.exit(2) + # Cursor option is only available in standalone mode + if not args.disable_cursor and not self.is_standalone(): + logger.critical("Cursor is only available in standalone mode") + sys.exit(2) + # Disable HDDTemp if sensors are disabled if getattr(self.args, 'disable_sensors', False): disable(self.args, 'hddtemp') diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index f3be8980..02796aa6 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -414,6 +414,8 @@ class _GlancesCurses(object): glances_processes.disable_extended() else: glances_processes.enable_extended() + # When a process is selected (and only in standalone mode), disable the cursor + self.args.disable_cursor = self.args.enable_process_extended and self.args.is_standalone elif self.pressedkey == ord('E'): # 'E' > Erase the process filter glances_processes.process_filter = None @@ -451,11 +453,11 @@ class _GlancesCurses(object): # ">" (right arrow) navigation through process sort next_sort = (self.loop_position() + 1) % len(self._sort_loop) glances_processes.set_sort_key(self._sort_loop[next_sort], False) - elif self.pressedkey == curses.KEY_UP or self.pressedkey == 65: + elif self.pressedkey == curses.KEY_UP or self.pressedkey == 65 and not self.args.disable_cursor: # 'UP' > Up in the server list if self.args.cursor_position > 0: self.args.cursor_position -= 1 - elif self.pressedkey == curses.KEY_DOWN or self.pressedkey == 66: + elif self.pressedkey == curses.KEY_DOWN or self.pressedkey == 66 and not self.args.disable_cursor: # 'DOWN' > Down in the server list # if self.args.cursor_position < glances_processes.max_processes - 2: if self.args.cursor_position < glances_processes.processes_count: diff --git a/glances/plugins/glances_processlist.py b/glances/plugins/glances_processlist.py index 7a9efd57..a7417e3c 100644 --- a/glances/plugins/glances_processlist.py +++ b/glances/plugins/glances_processlist.py @@ -353,11 +353,11 @@ class Plugin(GlancesPlugin): - 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 - if args.is_standalone: - ret.append(self.curse_add_line(unicode_message('PROCESS_SELECTOR') if selected else ' ', 'SELECTED')) + 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)) @@ -404,7 +404,7 @@ class Plugin(GlancesPlugin): cmdline = p.get('cmdline', '?') try: - process_decoration = 'PROCESS_SELECTED' if (selected and args.is_standalone) else 'PROCESS' + 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) @@ -428,74 +428,14 @@ class Plugin(GlancesPlugin): logger.debug("Can not decode command line '{}' ({})".format(cmdline, e)) ret.append(self.curse_add_line('', splittable=True)) - # Add extended stats but only for the top processes - if args.cursor_position == 0 and 'extended_stats' in p and args.enable_process_extended: - # Left padding - xpad = ' ' * 13 - # First line is CPU affinity - if 'cpu_affinity' in p and p['cpu_affinity'] is not None: - ret.append(self.curse_new_line()) - msg = xpad + 'CPU affinity: ' + str(len(p['cpu_affinity'])) + ' cores' - ret.append(self.curse_add_line(msg, splittable=True)) - # Second line is memory info - if 'memory_info' in p and p['memory_info'] is not None: - ret.append(self.curse_new_line()) - msg = '{}Memory info: {}'.format(xpad, p['memory_info']) - if 'memory_swap' in p and p['memory_swap'] is not None: - msg += ' swap ' + self.auto_unit(p['memory_swap'], low_precision=False) - ret.append(self.curse_add_line(msg, splittable=True)) - # Third line is for open files/network sessions - msg = '' - if 'num_threads' in p and p['num_threads'] is not None: - msg += str(p['num_threads']) + ' threads ' - if 'num_fds' in p and p['num_fds'] is not None: - msg += str(p['num_fds']) + ' files ' - if 'num_handles' in p and p['num_handles'] is not None: - msg += str(p['num_handles']) + ' handles ' - if 'tcp' in p and p['tcp'] is not None: - msg += str(p['tcp']) + ' TCP ' - if 'udp' in p and p['udp'] is not None: - msg += str(p['udp']) + ' UDP' - if msg != '': - ret.append(self.curse_new_line()) - msg = xpad + 'Open: ' + msg - ret.append(self.curse_add_line(msg, splittable=True)) - # Fourth line is IO nice level (only Linux and Windows OS) - if 'ionice' in p and p['ionice'] is not None and hasattr(p['ionice'], 'ioclass'): - ret.append(self.curse_new_line()) - msg = xpad + '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)) - 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 @@ -507,6 +447,16 @@ class Plugin(GlancesPlugin): # 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) @@ -515,10 +465,10 @@ class Plugin(GlancesPlugin): # Loop over processes (sorted by the sort key previously compute) # This is a Glances bottleneck (see flame graph), # get_process_curses_data should be optimzed - i = 0 - for p in self.__sort_stats(process_sort_key): - ret.extend(self.get_process_curses_data(p, i == args.cursor_position, args)) - i += 1 + 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: @@ -532,6 +482,95 @@ class Plugin(GlancesPlugin): # Return the message with decoration return ret + def __msg_curse_extended_process(self, ret, p): + """Get extended curses data tfor the selected process (see issue #2225) + p: the process dict + {'status': 'S', + 'memory_info': pmem(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': pgids(real=1000, effective=1000, saved=1000), + 'cpu_times': pcputimes(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'} + """ + # Title + msg = 'Selected {} {}'.format('program' if self.args.programs else 'thread', + p['name']) + ret.append(self.curse_add_line(msg, "TITLE")) + + # First line is CPU affinity + if 'cpu_affinity' in p and p['cpu_affinity'] is not None: + ret.append(self.curse_new_line()) + msg = 'CPU affinity: ' + str(len(p['cpu_affinity'])) + ' cores' + ret.append(self.curse_add_line(msg, splittable=True)) + # Second line is memory info + if 'memory_info' in p and p['memory_info'] is not None: + ret.append(self.curse_new_line()) + msg = 'Memory info: {}'.format(p['memory_info']) + if 'memory_swap' in p and p['memory_swap'] is not None: + msg += ' swap ' + self.auto_unit(p['memory_swap'], low_precision=False) + ret.append(self.curse_add_line(msg, splittable=True)) + # Third line is for open files/network sessions + msg = '' + if 'num_threads' in p and p['num_threads'] is not None: + msg += str(p['num_threads']) + ' threads ' + if 'num_fds' in p and p['num_fds'] is not None: + msg += str(p['num_fds']) + ' files ' + if 'num_handles' in p and p['num_handles'] is not None: + msg += str(p['num_handles']) + ' handles ' + if 'tcp' in p and p['tcp'] is not None: + msg += str(p['tcp']) + ' TCP ' + if 'udp' in p and p['udp'] is not None: + msg += str(p['udp']) + ' UDP' + if msg != '': + ret.append(self.curse_new_line()) + msg = 'Open: ' + msg + ret.append(self.curse_add_line(msg, splittable=True)) + # Fourth line is IO nice level (only Linux and Windows OS) + if 'ionice' in p and p['ionice'] is not None and hasattr(p['ionice'], 'ioclass'): + ret.append(self.curse_new_line()) + 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)) + + 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' diff --git a/glances/processes.py b/glances/processes.py index 0d41c5be..873983f4 100644 --- a/glances/processes.py +++ b/glances/processes.py @@ -37,6 +37,10 @@ class GlancesProcesses(object): def __init__(self, cache_timeout=60): """Init the class to collect stats about processes.""" + # Init the args, coming from the GlancesStandalone class + # Should be set by the set_args method + self.args = None + # Add internals caches because psutil do not cache all the stats # See: https://github.com/giampaolo/psutil/issues/462 self.username_cache = {} @@ -70,6 +74,7 @@ class GlancesProcesses(object): # Extended stats for top process is enable by default self.disable_extended_tag = False + self.extended_process = None # Test if the system can grab io_counters try: @@ -109,6 +114,10 @@ class GlancesProcesses(object): self._max_values = {} self.reset_max_values() + def set_args(self, args): + """Set args.""" + self.args = args + def reset_processcount(self): """Reset the global process count""" self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0, 'pid_max': None} @@ -247,6 +256,64 @@ class GlancesProcesses(object): for k in self._max_values_list: self._max_values[k] = 0.0 + def get_extended_stats(self, proc): + """Get the extended stats for the given PID.""" + # - cpu_affinity (Linux, Windows, FreeBSD) + # - ionice (Linux and Windows > Vista) + # - num_ctx_switches (not available on Illumos/Solaris) + # - num_fds (Unix-like) + # - num_handles (Windows) + # - memory_maps (only swap, Linux) + # https://www.cyberciti.biz/faq/linux-which-process-is-using-swap/ + # - connections (TCP and UDP) + ret = {} + try: + selected_process = psutil.Process(proc['pid']) + extended_stats = ['cpu_affinity', 'ionice', 'num_ctx_switches'] + if LINUX: + # num_fds only available on Unix system (see issue #1351) + extended_stats += ['num_fds'] + if WINDOWS: + extended_stats += ['num_handles'] + + # Get the extended stats + ret = selected_process.as_dict(attrs=extended_stats, ad_value=None) + + if LINUX: + try: + ret['memory_swap'] = sum([v.swap for v in selected_process.memory_maps()]) + except (psutil.NoSuchProcess, KeyError): + # (KeyError catch for issue #1551) + pass + except (psutil.AccessDenied, NotImplementedError): + # NotImplementedError: /proc/${PID}/smaps file doesn't exist + # on kernel < 2.6.14 or CONFIG_MMU kernel configuration option + # is not enabled (see psutil #533/glances #413). + ret['memory_swap'] = None + try: + ret['tcp'] = len(selected_process.connections(kind="tcp")) + ret['udp'] = len(selected_process.connections(kind="udp")) + except (psutil.AccessDenied, psutil.NoSuchProcess): + # Manage issue1283 (psutil.AccessDenied) + ret['tcp'] = None + ret['udp'] = None + except (psutil.NoSuchProcess, ValueError, AttributeError) as e: + logger.error('Can not grab extended stats ({})'.format(e)) + ret['extended_stats'] = False + self.extended_process = None + else: + logger.debug('Grab extended stats for process {}'.format(proc['pid'])) + ret['extended_stats'] = True + return ret + + def is_selected_process(self, position): + """Return True if the process is the selected one.""" + return self.args.enable_process_extended and \ + not self.disable_extended_tag and \ + hasattr(self.args, 'cursor_position') and \ + position == self.args.cursor_position and \ + not self.args.disable_cursor + def update(self): """Update the processes stats.""" # Reset the stats @@ -303,59 +370,26 @@ class GlancesProcesses(object): # Update the processcount self.update_processcount(self.processlist) - # Loop over processes and add metadata - first = True - for proc in self.processlist: - # Get extended stats, only for top processes (see issue #403). - if first and not self.disable_extended_tag: - # - cpu_affinity (Linux, Windows, FreeBSD) - # - ionice (Linux and Windows > Vista) - # - num_ctx_switches (not available on Illumos/Solaris) - # - num_fds (Unix-like) - # - num_handles (Windows) - # - memory_maps (only swap, Linux) - # https://www.cyberciti.biz/faq/linux-which-process-is-using-swap/ - # - connections (TCP and UDP) - extended = {} - try: - top_process = psutil.Process(proc['pid']) - extended_stats = ['cpu_affinity', 'ionice', 'num_ctx_switches'] - if LINUX: - # num_fds only available on Unix system (see issue #1351) - extended_stats += ['num_fds'] - if WINDOWS: - extended_stats += ['num_handles'] + # Loop over processes and : + # - add extended stats for selected process + # - add metadata + for position, proc in enumerate(self.processlist): + # Extended stats + ################ - # Get the extended stats - extended = top_process.as_dict(attrs=extended_stats, ad_value=None) + # Get the selected process + if self.is_selected_process(position): + # logger.info('Selected process: {}'.format(proc)) + self.extended_process = proc - if LINUX: - try: - extended['memory_swap'] = sum([v.swap for v in top_process.memory_maps()]) - except (psutil.NoSuchProcess, KeyError): - # (KeyError catch for issue #1551) - pass - except (psutil.AccessDenied, NotImplementedError): - # NotImplementedError: /proc/${PID}/smaps file doesn't exist - # on kernel < 2.6.14 or CONFIG_MMU kernel configuration option - # is not enabled (see psutil #533/glances #413). - extended['memory_swap'] = None - try: - extended['tcp'] = len(top_process.connections(kind="tcp")) - extended['udp'] = len(top_process.connections(kind="udp")) - except (psutil.AccessDenied, psutil.NoSuchProcess): - # Manage issue1283 (psutil.AccessDenied) - extended['tcp'] = None - extended['udp'] = None - except (psutil.NoSuchProcess, ValueError, AttributeError) as e: - logger.error('Can not grab extended stats ({})'.format(e)) - extended['extended_stats'] = False - else: - logger.debug('Grab extended stats for process {}'.format(proc['pid'])) - extended['extended_stats'] = True - proc.update(extended) - first = False - # /End of extended stats + # Grab extended stats only for the selected process (see issue #2225) + if self.extended_process is not None and \ + proc['pid'] == self.extended_process['pid']: + self.extended_process = proc + proc.update(self.get_extended_stats(self.extended_process)) + + # Meta data + ########### # PID is the key proc['key'] = 'pid' diff --git a/glances/standalone.py b/glances/standalone.py index c1268580..9d5aadc0 100644 --- a/glances/standalone.py +++ b/glances/standalone.py @@ -51,6 +51,9 @@ class GlancesStandalone(object): self.display_modules_list() sys.exit(0) + # The args is needed to get the selected process in the process list (Curses mode) + glances_processes.set_args(args) + # If process extended stats is disabled by user if not args.enable_process_extended: logger.debug("Extended stats for top process are disabled")