Merge branch 'issue1444' into develop

This commit is contained in:
nicolargo 2020-11-14 12:04:13 +01:00
commit a9502af99b
9 changed files with 208 additions and 57 deletions

View File

@ -463,6 +463,7 @@ port=5672
user=guest
password=guest
queue=glances_queue
#protocol=amqps
[mqtt]
# Configuration for the --export mqtt option

View File

@ -128,6 +128,9 @@ The extended stats feature can be enabled using the
``--enable-process-extended`` option (command line) or the ``e`` key
(curses interface).
In curses/standalone mode, you can select a process using ``UP`` and ``DOWN`` and press:
- ``k`` to kill the selected process
.. note::
Limit for CPU and MEM percent values can be overwritten in the
configuration file under the ``[processlist]`` section. It is also

View File

@ -18,3 +18,6 @@ There is no alert on this information.
.. note::
This plugin is disabled by default in the configuration file.
To enable it just use the following option:
# glances --enable-plugin sensors

View File

@ -15,6 +15,7 @@ following:
user=glances
password=glances
queue=glances_queue
#protocol=amqps
and run Glances with:

View File

@ -395,6 +395,9 @@ Sort processes by I/O rate
Show/hide IP module
.TP
.B \fBk\fP
Kill selected process (only in curses/standalone mode)
.TP
.B \fBK\fP
Show/hide TCP connections
.TP
.B \fBl\fP
@ -480,6 +483,12 @@ Enable/disable mean GPU mode
.B \fB/\fP
Switch between process command line or command name
.TP
.B \fBUP\fP
Up in the processes list
.TP
.B \fBDOWN\fP
Down in the processes list
.TP
.B \fBF5\fP
Refresh stats in curses user interface
.UNINDENT

View File

@ -419,6 +419,13 @@ Examples of use:
disable(args, 'hddtemp')
logger.debug("Sensors and HDDTemp are disabled")
# Let the plugins known the Glances mode
self.args.is_standalone = self.is_standalone()
self.args.is_client = self.is_client()
self.args.is_client_browser = self.is_client_browser()
self.args.is_server = self.is_server()
self.args.is_webserver = self.is_webserver()
return args
def is_standalone(self):

View File

@ -50,10 +50,13 @@ class _GlancesCurses(object):
"""
_hotkeys = {
# 'ENTER' > Edit the process filter
'0': {'switch': 'disable_irix'},
'1': {'switch': 'percpu'},
'2': {'switch': 'disable_left_sidebar'},
'3': {'switch': 'disable_quicklook'},
# '4' > Enable or disable quicklook
# '5' > Enable or disable top menu
'6': {'switch': 'meangpu'},
'/': {'switch': 'process_short_name'},
'a': {'sort_key': 'auto'},
@ -64,13 +67,17 @@ class _GlancesCurses(object):
'C': {'switch': 'disable_cloud'},
'd': {'switch': 'disable_diskio'},
'D': {'switch': 'disable_docker'},
# 'e' > Enable/Disable process extended
# 'E' > Erase the process filter
# 'f' > Show/hide fs / folder stats
'F': {'switch': 'fs_free_space'},
'g': {'switch': 'generate_graph'},
'G': {'switch': 'disable_gpu'},
'h': {'switch': 'help_tag'},
'i': {'sort_key': 'io_counters'},
'I': {'switch': 'disable_ip'},
'k': {'switch': 'disable_connections'},
# 'k' > Kill selected process
'K': {'switch': 'disable_connections'},
'l': {'switch': 'disable_alert'},
'm': {'sort_key': 'memory_percent'},
'M': {'switch': 'reset_minmax_tag'},
@ -78,6 +85,7 @@ class _GlancesCurses(object):
'N': {'switch': 'disable_now'},
'p': {'sort_key': 'name'},
'P': {'switch': 'disable_ports'},
# 'q' or ESCAPE > Quit
'Q': {'switch': 'enable_irq'},
'r': {'switch': 'disable_smart'},
'R': {'switch': 'disable_raid'},
@ -87,7 +95,14 @@ class _GlancesCurses(object):
'T': {'switch': 'network_sum'},
'u': {'sort_key': 'username'},
'U': {'switch': 'network_cumul'},
# 'w' > Delete finished warning logs
'W': {'switch': 'disable_wifi'},
# 'x' > Delete finished warning and critical logs
# 'z' > Enable or disable processes
# "<" (left arrow) navigation through process sort
# ">" (right arrow) navigation through process sort
# 'UP' > Up in the server list
# 'DOWN' > Down in the server list
}
_sort_loop = ['cpu_percent', 'memory_percent', 'username',
@ -144,9 +159,15 @@ class _GlancesCurses(object):
# Init edit filter tag
self.edit_filter = False
# Init kill process tag
self.kill_process = False
# Init the process min/max reset
self.args.reset_minmax_tag = False
# Init cursor
self.args.cursor_position = 0
# Catch key pressed with non blocking mode
self.term_window.keypad(1)
self.term_window.nodelay(1)
@ -188,6 +209,8 @@ class _GlancesCurses(object):
try:
if hasattr(curses, 'start_color'):
curses.start_color()
logger.debug(
'Curses interface compatible with {} colors'.format(curses.COLORS))
if hasattr(curses, 'use_default_colors'):
curses.use_default_colors()
except Exception as e:
@ -226,35 +249,35 @@ class _GlancesCurses(object):
curses.init_pair(8, curses.COLOR_BLUE, -1)
# Colors text styles
if curses.COLOR_PAIRS > 8:
try:
curses.init_pair(9, curses.COLOR_MAGENTA, -1)
except Exception:
if self.is_theme('white'):
curses.init_pair(9, curses.COLOR_BLACK, -1)
else:
curses.init_pair(9, curses.COLOR_WHITE, -1)
try:
curses.init_pair(10, curses.COLOR_CYAN, -1)
except Exception:
if self.is_theme('white'):
curses.init_pair(10, curses.COLOR_BLACK, -1)
else:
curses.init_pair(10, curses.COLOR_WHITE, -1)
self.ifWARNING_color2 = curses.color_pair(9) | A_BOLD
self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD
self.filter_color = curses.color_pair(10) | A_BOLD
self.no_color = curses.color_pair(1)
self.default_color = curses.color_pair(3) | A_BOLD
self.nice_color = curses.color_pair(9)
self.cpu_time_color = curses.color_pair(9)
self.nice_color = curses.color_pair(5)
self.cpu_time_color = curses.color_pair(5)
self.ifCAREFUL_color = curses.color_pair(4) | A_BOLD
self.ifWARNING_color = curses.color_pair(5) | A_BOLD
self.ifCRITICAL_color = curses.color_pair(2) | A_BOLD
self.default_color2 = curses.color_pair(7)
self.ifCAREFUL_color2 = curses.color_pair(8) | A_BOLD
self.ifWARNING_color2 = curses.color_pair(5) | A_BOLD
self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD
self.filter_color = A_BOLD
self.selected_color = A_BOLD
if curses.COLOR_PAIRS > 8:
colors_list = [curses.COLOR_MAGENTA, curses.COLOR_CYAN, curses.COLOR_YELLOW]
for i in range(0, 3):
try:
curses.init_pair(i + 9, colors_list[i], -1)
except Exception:
if self.is_theme('white'):
curses.init_pair(i + 9, curses.COLOR_BLACK, -1)
else:
curses.init_pair(i + 9, curses.COLOR_WHITE, -1)
self.nice_color = curses.color_pair(9)
self.cpu_time_color = curses.color_pair(9)
self.ifWARNING_color2 = curses.color_pair(9) | A_BOLD
self.filter_color = curses.color_pair(10) | A_BOLD
self.selected_color = curses.color_pair(11) | A_BOLD
else:
# The screen is NOT compatible with a colored design
@ -271,6 +294,7 @@ class _GlancesCurses(object):
self.ifWARNING_color2 = A_BOLD
self.ifCRITICAL_color2 = curses.A_REVERSE
self.filter_color = A_BOLD
self.selected_color = A_BOLD
# Define the colors list (hash table) for stats
self.colors_list = {
@ -283,6 +307,7 @@ class _GlancesCurses(object):
'FILTER': self.filter_color,
'TITLE': self.title_color,
'PROCESS': self.default_color2,
'PROCESS_SELECTED': self.default_color2 | curses.A_UNDERLINE,
'STATUS': self.default_color2,
'NICE': self.nice_color,
'CPU_TIME': self.cpu_time_color,
@ -293,7 +318,8 @@ class _GlancesCurses(object):
'CAREFUL_LOG': self.ifCAREFUL_color,
'WARNING_LOG': self.ifWARNING_color,
'CRITICAL_LOG': self.ifCRITICAL_color,
'PASSWORD': curses.A_PROTECT
'PASSWORD': curses.A_PROTECT,
'SELECTED': self.selected_color
}
def set_cursor(self, value):
@ -331,22 +357,18 @@ class _GlancesCurses(object):
self._hotkeys[hotkey]['sort_key'] == 'auto')
# Other actions...
if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
# 'ESC'|'q' > Quit
if return_to_browser:
logger.info("Stop Glances client and return to the browser")
else:
logger.info("Stop Glances (keypressed: {})".format(self.pressedkey))
elif self.pressedkey == ord('\n'):
if self.pressedkey == ord('\n'):
# 'ENTER' > Edit the process filter
self.edit_filter = not self.edit_filter
elif self.pressedkey == ord('4'):
# '4' > Enable or disable quicklook
self.args.full_quicklook = not self.args.full_quicklook
if self.args.full_quicklook:
self.enable_fullquicklook()
else:
self.disable_fullquicklook()
elif self.pressedkey == ord('5'):
# '5' > Enable or disable top menu
self.args.disable_top = not self.args.disable_top
if self.args.disable_top:
self.disable_top()
@ -366,6 +388,9 @@ class _GlancesCurses(object):
# 'f' > Show/hide fs / folder stats
self.args.disable_fs = not self.args.disable_fs
self.args.disable_folders = not self.args.disable_folders
elif self.pressedkey == ord('k'):
# 'k' > Kill selected process (after confirmation)
self.kill_process = not self.kill_process
elif self.pressedkey == ord('w'):
# 'w' > Delete finished warning logs
glances_events.clean()
@ -387,6 +412,22 @@ 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:
# '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:
# '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:
self.args.cursor_position += 1
elif self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'):
# 'ESC'|'q' > Quit
if return_to_browser:
logger.info("Stop Glances client and return to the browser")
else:
logger.info(
"Stop Glances (keypressed: {})".format(self.pressedkey))
elif self.pressedkey == curses.KEY_F5:
# "F5" manual refresh requested
pass
@ -594,13 +635,39 @@ class _GlancesCurses(object):
'- cmdline:.*glances.*\n' +
'- username:nicolargo\n' +
'- username:^root ',
is_input=True,
popup_type='input',
input_value=glances_processes.process_filter_input)
glances_processes.process_filter = new_filter
elif self.edit_filter and cs_status is not None:
self.display_popup('Process filter only available in standalone mode')
self.edit_filter = False
# Display kill process confirmation popup
# Only in standalone mode (cs_status is None)
if self.kill_process and cs_status is None:
selected_process_raw = stats.get_plugin('processlist').get_raw()[
self.args.cursor_position]
confirm = self.display_popup(
'Kill process: {} (pid: {}) ?\n\nConfirm ([y]es/[n]o): '.format(
selected_process_raw['name'],
selected_process_raw['pid']),
popup_type='yesno')
if confirm.lower().startswith('y'):
try:
ret_kill = glances_processes.kill(selected_process_raw['pid'])
except Exception as e:
logger.error('Can not kill process {} ({})'.format(
selected_process_raw['name'], e))
else:
logger.info('Kill signal has been sent to process {} (return code: {})'.format(
selected_process_raw['name'], ret_kill))
elif self.kill_process and cs_status is not None:
self.display_popup(
'Kill process only available in standalone mode')
self.kill_process = False
# Display graph generation popup
if self.args.generate_graph:
self.display_popup('Generate graph in {}'.format(self.args.export_graph_path))
@ -756,30 +823,37 @@ class _GlancesCurses(object):
def display_popup(self, message,
size_x=None, size_y=None,
duration=3,
is_input=False,
popup_type='info',
input_size=30,
input_value=None):
"""
Display a centered popup.
If is_input is False:
popup_type='info'
Just an infotmation popup, no user interaction
Display a centered popup with the given message during duration seconds
If size_x and size_y: set the popup size
else set it automatically
Return True if the popup could be displayed
If is_input is True:
popup_type='input'
Display a centered popup with the given message and a input field
If size_x and size_y: set the popup size
else set it automatically
Return the input string or None if the field is empty
popup_type='yesno'
Display a centered popup with the given message
If size_x and size_y: set the popup size
else set it automatically
Return True (yes) or False (no)
"""
# Center the popup
sentence_list = message.split('\n')
if size_x is None:
size_x = len(max(sentence_list, key=len)) + 4
# Add space for the input field
if is_input:
if popup_type == 'input':
size_x += input_size
if size_y is None:
size_y = len(sentence_list) + 4
@ -798,10 +872,15 @@ class _GlancesCurses(object):
popup.border()
# Add the message
for y, m in enumerate(message.split('\n')):
for y, m in enumerate(sentence_list):
popup.addnstr(2 + y, 2, m, len(m))
if is_input:
if popup_type == 'info':
# Display the popup
popup.refresh()
self.wait(duration * 1000)
return True
elif popup_type == 'input':
# Create a subwindow for the text field
subpop = popup.derwin(1, input_size, 2, 2 + len(m))
subpop.attron(self.colors_list['FILTER'])
@ -817,7 +896,7 @@ class _GlancesCurses(object):
textbox = GlancesTextbox(subpop, insert_mode=False)
textbox.edit()
self.set_cursor(0)
self.term_window.keypad(0)
# self.term_window.keypad(0)
if textbox.gather() != '':
logger.debug(
"User enters the following string: %s" % textbox.gather())
@ -825,11 +904,23 @@ class _GlancesCurses(object):
else:
logger.debug("User centers an empty string")
return None
else:
elif popup_type == 'yesno':
# # Create a subwindow for the text field
subpop = popup.derwin(1, 2, len(sentence_list) + 1, len(m) + 2)
subpop.attron(self.colors_list['FILTER'])
# Init the field with the current value
subpop.addnstr(0, 0, '', 0)
# Display the popup
popup.refresh()
self.wait(duration * 1000)
return True
subpop.refresh()
# Create the textbox inside the subwindows
self.set_cursor(2)
self.term_window.keypad(1)
textbox = GlancesTextboxYesNo(subpop, insert_mode=False)
textbox.edit()
self.set_cursor(0)
# self.term_window.keypad(0)
return textbox.gather()
def display_plugin(self, plugin_stats,
display_optional=True,
@ -1055,3 +1146,12 @@ class GlancesTextbox(Textbox, object):
if ch == 127: # Back
return 8
return super(GlancesTextbox, self).do_command(ch)
class GlancesTextboxYesNo(Textbox, object):
def __init__(self, *args, **kwargs):
super(GlancesTextboxYesNo, self).__init__(*args, **kwargs)
def do_command(self, ch):
return super(GlancesTextboxYesNo, self).do_command(ch)

View File

@ -70,12 +70,13 @@ class Plugin(GlancesPlugin):
'status': '{:>1} ',
'ior': '{:>4} ',
'iow': '{:<4} ',
'command': '{}',
'command': '{} {}',
}
# Define the stat layout of the processes list columns
layout_stat = {
'cpu': '{:<6.1f} ',
'cpu': '{:<6.1f}',
'cpu_no_digit': '{:<6.0f}',
'mem': '{:<5.1f} ',
'virt': '{:<5} ',
'res': '{:<5} ',
@ -172,19 +173,26 @@ class Plugin(GlancesPlugin):
pass
return 'DEFAULT'
def get_process_curses_data(self, p, first, args):
def get_process_curses_data(self, p, selected, args):
"""Get curses data to display for a process.
- p is the process to display
- first is a tag=True if the process is the first on the list
- selected is a tag=True if the selected process
"""
ret = [self.curse_new_line()]
# When a process is selected:
# * display a special caracter at the beginning of the line
# * underline the command name
if args.is_standalone:
ret.append(self.curse_add_line('>' if selected else ' ', 'SELECTED'))
# CPU
if 'cpu_percent' in p and p['cpu_percent'] is not None and p['cpu_percent'] != '':
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 = self.layout_stat['cpu'].format(p['cpu_percent'] / float(self.nb_log_core))
msg = cpu_layout.format(
p['cpu_percent'] / float(self.nb_log_core))
else:
msg = self.layout_stat['cpu'].format(p['cpu_percent'])
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']),
@ -321,28 +329,31 @@ class Plugin(GlancesPlugin):
else:
cmdline = '?'
try:
process_decoration = 'PROCESS_SELECTED' if (selected and args.is_standalone) else 'PROCESS'
if cmdline:
path, cmd, arguments = split_cmdline(cmdline)
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', 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', splittable=True))
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(p['name'])
ret.append(self.curse_add_line(msg, splittable=True))
ret.append(self.curse_add_line(msg, decoration=process_decoration, splittable=True))
except (TypeError, UnicodeEncodeError) as e:
# Avoid crach 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))
# Add extended stats but only for the top processes
if first and 'extended_stats' in p and args.enable_process_extended:
if args.cursor_position == 0 and 'extended_stats' in p and args.enable_process_extended:
# Left padding
xpad = ' ' * 13
# First line is CPU affinity
@ -429,11 +440,13 @@ class Plugin(GlancesPlugin):
# Process list
# Loop over processes (sorted by the sort key previously compute)
first = True
i = 0
for p in self.__sort_stats(process_sort_key):
ret.extend(self.get_process_curses_data(p, first, args))
# End of extended stats
first = False
ret.extend(self.get_process_curses_data(
p, i == args.cursor_position, args))
i += 1
# 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
@ -478,7 +491,8 @@ class Plugin(GlancesPlugin):
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))
msg = self.layout_header['command'].format('Command')
msg = self.layout_header['command'].format('Command',
"('k' to kill)" if args.is_standalone else "")
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):

View File

@ -176,6 +176,11 @@ class GlancesProcesses(object):
else:
return None
@property
def processes_count(self):
"""Get the current number of processes showed in the UI."""
return min(self._max_processes - 2, glances_processes.processcount['total'] - 1)
@property
def max_processes(self):
"""Get the maximum number of processes showed in the UI."""
@ -398,6 +403,14 @@ class GlancesProcesses(object):
else:
self.auto_sort = auto
self._sort_key = key
def kill(self, pid, timeout=3):
"""Kill process with pid"""
assert pid != os.getpid(), "Glances can kill itself..."
p = psutil.Process(pid)
logger.debug('Send kill signal to process: {}'.format(p))
p.kill()
return p.wait(timeout)
def weighted(value):