First version but UI should be improved and when user is in program mode, it did not work...

This commit is contained in:
nicolargo 2022-12-27 10:45:51 +01:00
parent 8bdbed3331
commit 9614e2bb19
5 changed files with 216 additions and 126 deletions

View File

@ -236,6 +236,13 @@ Examples of use:
dest='enable_separator', dest='enable_separator',
help='enable separator in the UI', 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 # Sort processes list
parser.add_argument( parser.add_argument(
'--sort-processes', '--sort-processes',
@ -700,6 +707,11 @@ Examples of use:
logger.critical("Process filter is only available in standalone mode") logger.critical("Process filter is only available in standalone mode")
sys.exit(2) 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 # Disable HDDTemp if sensors are disabled
if getattr(self.args, 'disable_sensors', False): if getattr(self.args, 'disable_sensors', False):
disable(self.args, 'hddtemp') disable(self.args, 'hddtemp')

View File

@ -414,6 +414,8 @@ class _GlancesCurses(object):
glances_processes.disable_extended() glances_processes.disable_extended()
else: else:
glances_processes.enable_extended() 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'): elif self.pressedkey == ord('E'):
# 'E' > Erase the process filter # 'E' > Erase the process filter
glances_processes.process_filter = None glances_processes.process_filter = None
@ -451,11 +453,11 @@ class _GlancesCurses(object):
# ">" (right arrow) navigation through process sort # ">" (right arrow) navigation through process sort
next_sort = (self.loop_position() + 1) % len(self._sort_loop) next_sort = (self.loop_position() + 1) % len(self._sort_loop)
glances_processes.set_sort_key(self._sort_loop[next_sort], False) 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 # 'UP' > Up in the server list
if self.args.cursor_position > 0: if self.args.cursor_position > 0:
self.args.cursor_position -= 1 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 # 'DOWN' > Down in the server list
# if self.args.cursor_position < glances_processes.max_processes - 2: # if self.args.cursor_position < glances_processes.max_processes - 2:
if self.args.cursor_position < glances_processes.processes_count: if self.args.cursor_position < glances_processes.processes_count:

View File

@ -353,11 +353,11 @@ class Plugin(GlancesPlugin):
- selected is a tag=True if p is the selected process - selected is a tag=True if p is the selected process
""" """
ret = [self.curse_new_line()] ret = [self.curse_new_line()]
# When a process is selected: # When a process is selected:
# * display a special character at the beginning of the line # * display a special character at the beginning of the line
# * underline the command name # * underline the command name
if args.is_standalone: ret.append(self.curse_add_line(unicode_message('PROCESS_SELECTOR') if (selected and not args.disable_cursor) else ' ', 'SELECTED'))
ret.append(self.curse_add_line(unicode_message('PROCESS_SELECTOR') if selected else ' ', 'SELECTED'))
# CPU # CPU
ret.append(self._get_process_curses_cpu(p, selected, args)) ret.append(self._get_process_curses_cpu(p, selected, args))
@ -404,7 +404,7 @@ class Plugin(GlancesPlugin):
cmdline = p.get('cmdline', '?') cmdline = p.get('cmdline', '?')
try: 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: if cmdline:
path, cmd, arguments = split_cmdline(bare_process_name, cmdline) path, cmd, arguments = split_cmdline(bare_process_name, cmdline)
# Manage end of line in arguments (see #1692) # 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)) logger.debug("Can not decode command line '{}' ({})".format(cmdline, e))
ret.append(self.curse_add_line('', splittable=True)) 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 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): def msg_curse(self, args=None, max_width=None):
"""Return the dict to display in the curse interface.""" """Return the dict to display in the curse interface."""
# Init the return message # Init the return message
@ -507,6 +447,16 @@ class Plugin(GlancesPlugin):
# Compute the sort key # Compute the sort key
process_sort_key = glances_processes.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 # Header
self.__msg_curse_header(ret, process_sort_key, args) 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) # Loop over processes (sorted by the sort key previously compute)
# This is a Glances bottleneck (see flame graph), # This is a Glances bottleneck (see flame graph),
# get_process_curses_data should be optimzed # get_process_curses_data should be optimzed
i = 0 for position, process in enumerate(processes_list_sorted):
for p in self.__sort_stats(process_sort_key): ret.extend(self.get_process_curses_data(process,
ret.extend(self.get_process_curses_data(p, i == args.cursor_position, args)) position == args.cursor_position,
i += 1 args))
# A filter is set Display the stats summaries # A filter is set Display the stats summaries
if glances_processes.process_filter is not None: if glances_processes.process_filter is not None:
@ -532,6 +482,95 @@ class Plugin(GlancesPlugin):
# Return the message with decoration # Return the message with decoration
return ret 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): def __msg_curse_header(self, ret, process_sort_key, args=None):
"""Build the header and add it to the ret dict.""" """Build the header and add it to the ret dict."""
sort_style = 'SORT' sort_style = 'SORT'

View File

@ -37,6 +37,10 @@ class GlancesProcesses(object):
def __init__(self, cache_timeout=60): def __init__(self, cache_timeout=60):
"""Init the class to collect stats about processes.""" """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 # Add internals caches because psutil do not cache all the stats
# See: https://github.com/giampaolo/psutil/issues/462 # See: https://github.com/giampaolo/psutil/issues/462
self.username_cache = {} self.username_cache = {}
@ -70,6 +74,7 @@ class GlancesProcesses(object):
# Extended stats for top process is enable by default # Extended stats for top process is enable by default
self.disable_extended_tag = False self.disable_extended_tag = False
self.extended_process = None
# Test if the system can grab io_counters # Test if the system can grab io_counters
try: try:
@ -109,6 +114,10 @@ class GlancesProcesses(object):
self._max_values = {} self._max_values = {}
self.reset_max_values() self.reset_max_values()
def set_args(self, args):
"""Set args."""
self.args = args
def reset_processcount(self): def reset_processcount(self):
"""Reset the global process count""" """Reset the global process count"""
self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0, 'pid_max': None} 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: for k in self._max_values_list:
self._max_values[k] = 0.0 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): def update(self):
"""Update the processes stats.""" """Update the processes stats."""
# Reset the stats # Reset the stats
@ -303,59 +370,26 @@ class GlancesProcesses(object):
# Update the processcount # Update the processcount
self.update_processcount(self.processlist) self.update_processcount(self.processlist)
# Loop over processes and add metadata # Loop over processes and :
first = True # - add extended stats for selected process
for proc in self.processlist: # - add metadata
# Get extended stats, only for top processes (see issue #403). for position, proc in enumerate(self.processlist):
if first and not self.disable_extended_tag: # Extended stats
# - 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']
# Get the extended stats # Get the selected process
extended = top_process.as_dict(attrs=extended_stats, ad_value=None) if self.is_selected_process(position):
# logger.info('Selected process: {}'.format(proc))
self.extended_process = proc
if LINUX: # Grab extended stats only for the selected process (see issue #2225)
try: if self.extended_process is not None and \
extended['memory_swap'] = sum([v.swap for v in top_process.memory_maps()]) proc['pid'] == self.extended_process['pid']:
except (psutil.NoSuchProcess, KeyError): self.extended_process = proc
# (KeyError catch for issue #1551) proc.update(self.get_extended_stats(self.extended_process))
pass
except (psutil.AccessDenied, NotImplementedError): # Meta data
# 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
# PID is the key # PID is the key
proc['key'] = 'pid' proc['key'] = 'pid'

View File

@ -51,6 +51,9 @@ class GlancesStandalone(object):
self.display_modules_list() self.display_modules_list()
sys.exit(0) 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 process extended stats is disabled by user
if not args.enable_process_extended: if not args.enable_process_extended:
logger.debug("Extended stats for top process are disabled") logger.debug("Extended stats for top process are disabled")