mirror of
https://github.com/nicolargo/glances.git
synced 2026-03-15 12:27:24 -04:00
Merge branch 'feature/issue405' into develop (#issue405)
Process filter patern feature is now available.
This commit is contained in:
5
NEWS
5
NEWS
@@ -5,6 +5,11 @@ Glances Version 2.x
|
||||
Version 2.1
|
||||
===========
|
||||
|
||||
* Add user process filter feature
|
||||
User can define a process filter pattern (as a regular expression).
|
||||
The pattern could be defined from the command line (-f <pattern>)
|
||||
or by pressing the ENTER key in the curse interface.
|
||||
Process filter feature is only available in standalone mode.
|
||||
* Create a max_processes key in the configuration file
|
||||
The goal is to reduce the number of displayed processes in the curses UI and
|
||||
so limit the CPU footprint of the Glances standalone mode.
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
# Import system libs
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
# Import Glances libs
|
||||
from glances.core.glances_config import Config
|
||||
@@ -111,6 +112,8 @@ class GlancesMain(object):
|
||||
parser.add_argument('-w', '--webserver', action='store_true', default=False,
|
||||
dest='webserver', help=_('run Glances in web server mode'))
|
||||
# Display (Curses) options
|
||||
parser.add_argument('-f', '--process-filter', default=None, type=str,
|
||||
dest='process_filter', help=_('set the process filter patern (regular expression)'))
|
||||
parser.add_argument('-b', '--byte', action='store_true', default=False,
|
||||
dest='byte', help=_('display network rate in byte per second'))
|
||||
parser.add_argument('-1', '--percpu', action='store_true', default=False,
|
||||
@@ -141,7 +144,7 @@ class GlancesMain(object):
|
||||
|
||||
# In web server mode, defaul refresh time: 5 sec
|
||||
if args.webserver:
|
||||
args.time = 5
|
||||
args.time = 5
|
||||
|
||||
# Server or client login/password
|
||||
args.username = self.username
|
||||
@@ -172,6 +175,14 @@ class GlancesMain(object):
|
||||
args.network_sum = False
|
||||
args.network_cumul = False
|
||||
|
||||
# Control parameter and exit if it is not OK
|
||||
self.args = args
|
||||
|
||||
# Filter is only available in standalone mode
|
||||
if args.process_filter is not None and not self.is_standalone():
|
||||
logger.critical(_("Process filter is only available in standalone mode"))
|
||||
sys.exit(2)
|
||||
|
||||
return args
|
||||
|
||||
def __hash_password(self, plain_password):
|
||||
|
||||
@@ -17,10 +17,13 @@
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Import Glances lib
|
||||
from glances.core.glances_globals import is_linux, is_bsd, is_mac, is_windows, logger
|
||||
from glances.core.glances_timer import Timer, getTimeSinceLastUpdate
|
||||
|
||||
# Import Python lib
|
||||
import psutil
|
||||
import re
|
||||
|
||||
|
||||
class GlancesProcesses(object):
|
||||
@@ -59,6 +62,13 @@ class GlancesProcesses(object):
|
||||
# None if no limit
|
||||
self.max_processes = None
|
||||
|
||||
# Process filter is a regular expression
|
||||
self.process_filter = None
|
||||
self.process_filter_re = None
|
||||
|
||||
# !!! ONLY FOR TEST
|
||||
# self.set_process_filter('.*python.*')
|
||||
|
||||
def enable(self):
|
||||
"""Enable process stats."""
|
||||
self.disable_tag = False
|
||||
@@ -86,16 +96,48 @@ class GlancesProcesses(object):
|
||||
"""Get the maximum number of processes showed in the UI interfaces"""
|
||||
return self.max_processes
|
||||
|
||||
def set_process_filter(self, value):
|
||||
"""Set the process filter"""
|
||||
logger.info(_("Set process filter to %s") % value)
|
||||
self.process_filter = value
|
||||
if value is not None:
|
||||
try:
|
||||
self.process_filter_re = re.compile(value)
|
||||
logger.debug(_("Process filter regular expression compilation OK: %s") % self.get_process_filter())
|
||||
except:
|
||||
logger.error(_("Can not compile process filter regular expression: %s") % value)
|
||||
self.process_filter_re = None
|
||||
else:
|
||||
self.process_filter_re = None
|
||||
return self.process_filter
|
||||
|
||||
def get_process_filter(self):
|
||||
"""Get the process filter"""
|
||||
return self.process_filter
|
||||
|
||||
def get_process_filter_re(self):
|
||||
"""Get the process regular expression compiled"""
|
||||
return self.process_filter_re
|
||||
|
||||
def is_filtered(self, value):
|
||||
"""Return True if the value should be filtered"""
|
||||
if self.get_process_filter() is None:
|
||||
# No filter => Not filtered
|
||||
return False
|
||||
else:
|
||||
# logger.debug(self.get_process_filter() + " <> " + value + " => " + str(self.get_process_filter_re().match(value) is None))
|
||||
return self.get_process_filter_re().match(value) is None
|
||||
|
||||
def __get_process_stats(self, proc,
|
||||
mandatory_stats=True,
|
||||
standard_stats=True,
|
||||
extended_stats=False):
|
||||
"""
|
||||
Get process stats of the proc processes (proc is returned psutil.process_iter())
|
||||
mandatory_stats: need for the sorting step
|
||||
=> cpu_percent, memory_percent, io_counters, name
|
||||
mandatory_stats: need for the sorting/filter step
|
||||
=> cpu_percent, memory_percent, io_counters, name, cmdline
|
||||
standard_stats: for all the displayed processes
|
||||
=> username, cmdline, status, memory_info, cpu_times
|
||||
=> username, status, memory_info, cpu_times
|
||||
extended_stats: only for top processes (see issue #403)
|
||||
=> connections (UDP/TCP), memory_swap...
|
||||
"""
|
||||
@@ -109,6 +151,17 @@ class GlancesProcesses(object):
|
||||
# Process CPU, MEM percent and name
|
||||
procstat.update(proc.as_dict(attrs=['cpu_percent', 'memory_percent', 'name'], ad_value=''))
|
||||
|
||||
# Process command line (cached with internal cache)
|
||||
try:
|
||||
self.cmdline_cache[procstat['pid']]
|
||||
except KeyError:
|
||||
# Patch for issue #391
|
||||
try:
|
||||
self.cmdline_cache[procstat['pid']] = ' '.join(proc.cmdline())
|
||||
except (AttributeError, psutil.AccessDenied, UnicodeDecodeError):
|
||||
self.cmdline_cache[procstat['pid']] = ""
|
||||
procstat['cmdline'] = self.cmdline_cache[procstat['pid']]
|
||||
|
||||
# Process IO
|
||||
# procstat['io_counters'] is a list:
|
||||
# [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag]
|
||||
@@ -155,17 +208,6 @@ class GlancesProcesses(object):
|
||||
self.username_cache[procstat['pid']] = "?"
|
||||
procstat['username'] = self.username_cache[procstat['pid']]
|
||||
|
||||
# Process command line (cached with internal cache)
|
||||
try:
|
||||
self.cmdline_cache[procstat['pid']]
|
||||
except KeyError:
|
||||
# Patch for issue #391
|
||||
try:
|
||||
self.cmdline_cache[procstat['pid']] = ' '.join(proc.cmdline())
|
||||
except (AttributeError, psutil.AccessDenied, UnicodeDecodeError):
|
||||
self.cmdline_cache[procstat['pid']] = ""
|
||||
procstat['cmdline'] = self.cmdline_cache[procstat['pid']]
|
||||
|
||||
# Process status, nice, memory_info and cpu_times
|
||||
procstat.update(proc.as_dict(attrs=['status', 'nice', 'memory_info', 'cpu_times']))
|
||||
procstat['status'] = str(procstat['status'])[:1].upper()
|
||||
@@ -237,9 +279,14 @@ class GlancesProcesses(object):
|
||||
for proc in psutil.process_iter():
|
||||
# If self.get_max_processes() is None: Only retreive mandatory stats
|
||||
# Else: retreive mandatoryadn standard stast
|
||||
processdict[proc] = self.__get_process_stats(proc,
|
||||
mandatory_stats=True,
|
||||
standard_stats=self.get_max_processes() is None)
|
||||
s = self.__get_process_stats(proc,
|
||||
mandatory_stats=True,
|
||||
standard_stats=self.get_max_processes() is None)
|
||||
# Continue to the next process if it has to be filtered
|
||||
if self.is_filtered(s['cmdline']) and self.is_filtered(s['name']):
|
||||
continue
|
||||
# Ok add the process to the list
|
||||
processdict[proc] = s
|
||||
# ignore the 'idle' process on Windows and *BSD
|
||||
# ignore the 'kernel_task' process on OS X
|
||||
# waiting for upstream patch from psutil
|
||||
|
||||
@@ -51,6 +51,10 @@ class GlancesStandalone(object):
|
||||
logger.debug(_("Extended stats for top process is enabled (default behavor)"))
|
||||
glances_processes.enable_extended()
|
||||
|
||||
# Manage optionnal process filter
|
||||
if args.process_filter is not None:
|
||||
glances_processes.set_process_filter(args.process_filter)
|
||||
|
||||
# Initial system informations update
|
||||
self.stats.update()
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ class GlancesBottle(object):
|
||||
'BOLD': 'bold',
|
||||
'SORT': 'sort',
|
||||
'OK': 'ok',
|
||||
'FILTER': 'filter',
|
||||
'TITLE': 'title',
|
||||
'CAREFUL': 'careful',
|
||||
'WARNING': 'warning',
|
||||
|
||||
@@ -31,6 +31,7 @@ if not is_windows:
|
||||
try:
|
||||
import curses
|
||||
import curses.panel
|
||||
from curses.textpad import Textbox, rectangle
|
||||
except ImportError:
|
||||
logger.critical('Curses module not found. Glances cannot start in standalone mode.')
|
||||
sys.exit(1)
|
||||
@@ -70,11 +71,7 @@ class GlancesCurses(object):
|
||||
curses.noecho()
|
||||
if hasattr(curses, 'cbreak'):
|
||||
curses.cbreak()
|
||||
if hasattr(curses, 'curs_set'):
|
||||
try:
|
||||
curses.curs_set(0)
|
||||
except Exception:
|
||||
pass
|
||||
self.set_cursor(0)
|
||||
|
||||
# Init colors
|
||||
self.hascolors = False
|
||||
@@ -93,6 +90,7 @@ class GlancesCurses(object):
|
||||
curses.init_pair(7, curses.COLOR_GREEN, -1)
|
||||
curses.init_pair(8, curses.COLOR_BLUE, -1)
|
||||
curses.init_pair(9, curses.COLOR_MAGENTA, -1)
|
||||
curses.init_pair(10, curses.COLOR_CYAN, -1)
|
||||
else:
|
||||
self.hascolors = False
|
||||
|
||||
@@ -116,6 +114,7 @@ class GlancesCurses(object):
|
||||
self.ifCAREFUL_color2 = curses.color_pair(8) | A_BOLD
|
||||
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
|
||||
else:
|
||||
# B&W text styles
|
||||
self.no_color = curses.A_NORMAL
|
||||
@@ -128,6 +127,7 @@ class GlancesCurses(object):
|
||||
self.ifCAREFUL_color2 = curses.A_UNDERLINE
|
||||
self.ifWARNING_color2 = A_BOLD
|
||||
self.ifCRITICAL_color2 = curses.A_REVERSE
|
||||
self.filter_color = A_BOLD
|
||||
|
||||
# Define the colors list (hash table) for stats
|
||||
self.__colors_list = {
|
||||
@@ -136,6 +136,7 @@ class GlancesCurses(object):
|
||||
'BOLD': A_BOLD,
|
||||
'SORT': A_BOLD,
|
||||
'OK': self.default_color2,
|
||||
'FILTER': self.filter_color,
|
||||
'TITLE': self.title_color,
|
||||
'PROCESS': self.default_color2,
|
||||
'STATUS': self.default_color2,
|
||||
@@ -158,6 +159,9 @@ class GlancesCurses(object):
|
||||
# Init process sort method
|
||||
self.args.process_sorted_by = 'auto'
|
||||
|
||||
# Init edit filter tag
|
||||
self.edit_filter = False
|
||||
|
||||
# Catch key pressed with non blocking mode
|
||||
self.term_window.keypad(1)
|
||||
self.term_window.nodelay(1)
|
||||
@@ -174,6 +178,18 @@ class GlancesCurses(object):
|
||||
args.enable_history = False
|
||||
logger.error('Stats history disabled because graph lib is not available')
|
||||
|
||||
def set_cursor(self, value):
|
||||
"""Configure the cursor
|
||||
0: invisible
|
||||
1: visible
|
||||
2: very visible
|
||||
"""
|
||||
if hasattr(curses, 'curs_set'):
|
||||
try:
|
||||
curses.curs_set(value)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def __get_key(self, window):
|
||||
# Catch ESC key AND numlock key (issue #163)
|
||||
keycode = [0, 0]
|
||||
@@ -197,6 +213,9 @@ class GlancesCurses(object):
|
||||
self.end()
|
||||
logger.info("Stop Glances")
|
||||
sys.exit(0)
|
||||
elif self.pressedkey == 10:
|
||||
# 'ENTER' > Edit the process filter
|
||||
self.edit_filter = not self.edit_filter
|
||||
elif self.pressedkey == ord('1'):
|
||||
# '1' > Switch between CPU and PerCPU information
|
||||
self.args.percpu = not self.args.percpu
|
||||
@@ -463,19 +482,44 @@ class GlancesCurses(object):
|
||||
self.history_tag = False
|
||||
self.reset_history_tag = False
|
||||
|
||||
# Display edit filter popup
|
||||
# Only in standalone mode (cs_status == 'None')
|
||||
if self.edit_filter and cs_status == 'None':
|
||||
new_filter = self.display_popup(_("Process filter pattern: "),
|
||||
is_input=True,
|
||||
input_value=glances_processes.get_process_filter())
|
||||
glances_processes.set_process_filter(new_filter)
|
||||
elif self.edit_filter and cs_status != 'None':
|
||||
self.display_popup(_("Process filter only available in standalone mode"))
|
||||
self.edit_filter = False
|
||||
|
||||
return True
|
||||
|
||||
def display_popup(self, message, size_x=None, size_y=None, duration=3):
|
||||
def display_popup(self, message,
|
||||
size_x=None, size_y=None,
|
||||
duration=3,
|
||||
is_input=False,
|
||||
input_size=30,
|
||||
input_value=None):
|
||||
"""
|
||||
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 False:
|
||||
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:
|
||||
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
|
||||
"""
|
||||
|
||||
# Center the popup
|
||||
if size_x is None:
|
||||
size_x = len(message) + 4
|
||||
# Add space for the input field
|
||||
if is_input:
|
||||
size_x += input_size
|
||||
if size_y is None:
|
||||
size_y = message.count('\n') + 1 + 4
|
||||
screen_x = self.screen.getmaxyx()[1]
|
||||
@@ -488,7 +532,7 @@ class GlancesCurses(object):
|
||||
|
||||
# Create the popup
|
||||
popup = curses.newwin(size_y, size_x, pos_y, pos_x)
|
||||
|
||||
|
||||
# Fill the popup
|
||||
popup.border()
|
||||
|
||||
@@ -498,11 +542,32 @@ class GlancesCurses(object):
|
||||
popup.addnstr(2 + y, 2, m, len(m))
|
||||
y += 1
|
||||
|
||||
# Display the popup
|
||||
popup.refresh()
|
||||
curses.napms(duration * 1000)
|
||||
|
||||
return True
|
||||
if is_input:
|
||||
# Create a subwindow for the text field
|
||||
subpop = popup.derwin(1, input_size, 2, 2 + len(m))
|
||||
subpop.attron(self.__colors_list['FILTER'])
|
||||
# Init the field with the current value
|
||||
if input_value is not None:
|
||||
subpop.addnstr(0, 0, input_value, len(input_value))
|
||||
# Display the popup
|
||||
popup.refresh()
|
||||
subpop.refresh()
|
||||
# Create the textbox inside the subwindows
|
||||
self.set_cursor(2)
|
||||
textbox = glances_textbox(subpop, insert_mode=False)
|
||||
textbox.edit()
|
||||
self.set_cursor(0)
|
||||
if textbox.gather() != '':
|
||||
logger.debug(_("User enters the following process filter patern: %s") % textbox.gather())
|
||||
return textbox.gather()[:-1]
|
||||
else:
|
||||
logger.debug(_("User clears the process filter patern"))
|
||||
return None
|
||||
else:
|
||||
# Display the popup
|
||||
popup.refresh()
|
||||
curses.napms(duration * 1000)
|
||||
return True
|
||||
|
||||
def display_plugin(self, plugin_stats,
|
||||
display_optional=True,
|
||||
@@ -648,3 +713,16 @@ class GlancesCurses(object):
|
||||
return 0
|
||||
else:
|
||||
return c + 1
|
||||
|
||||
class glances_textbox(Textbox):
|
||||
"""
|
||||
"""
|
||||
def __init__(*args, **kwargs):
|
||||
Textbox.__init__(*args, **kwargs)
|
||||
|
||||
def do_command(self, ch):
|
||||
if ch == 10: # Enter
|
||||
return 0
|
||||
if ch == 127: # Enter
|
||||
return 8
|
||||
return Textbox.do_command(self, ch)
|
||||
@@ -61,6 +61,9 @@ div#newline{
|
||||
#ok {
|
||||
color: green;
|
||||
}
|
||||
#filter {
|
||||
color: cyan;
|
||||
}
|
||||
#ok_log {
|
||||
background-color: green;
|
||||
color: white;
|
||||
|
||||
@@ -129,5 +129,11 @@ class Plugin(GlancesPlugin):
|
||||
msg = msg_col2.format("q", _("Quit (Esc and Ctrl-C also work)"))
|
||||
ret.append(self.curse_add_line(msg))
|
||||
|
||||
ret.append(self.curse_new_line())
|
||||
ret.append(self.curse_new_line())
|
||||
msg = '{0}: {1}'.format("ENTER", _("Edit the process filter patern"))
|
||||
ret.append(self.curse_add_line(msg))
|
||||
|
||||
|
||||
# Return the message with decoration
|
||||
return ret
|
||||
|
||||
@@ -69,9 +69,6 @@ class Plugin(GlancesPlugin):
|
||||
ret = []
|
||||
|
||||
# Only process if stats exist and display plugin enable...
|
||||
# if self.stats == {} or args.disable_process:
|
||||
# return ret
|
||||
|
||||
if args.disable_process:
|
||||
msg = _("PROCESSES DISABLED (press 'z' to display)")
|
||||
ret.append(self.curse_add_line(msg))
|
||||
@@ -80,6 +77,16 @@ class Plugin(GlancesPlugin):
|
||||
if self.stats == {}:
|
||||
return ret
|
||||
|
||||
# Display the filter (if it exists)
|
||||
if glances_processes.get_process_filter() is not None:
|
||||
msg = _("Processes filter:")
|
||||
ret.append(self.curse_add_line(msg, "TITLE"))
|
||||
msg = _(" {0} ").format(glances_processes.get_process_filter())
|
||||
ret.append(self.curse_add_line(msg, "FILTER"))
|
||||
msg = _("(press ENTER to edit)")
|
||||
ret.append(self.curse_add_line(msg))
|
||||
ret.append(self.curse_new_line())
|
||||
|
||||
# Build the string message
|
||||
# Header
|
||||
msg = _("TASKS ")
|
||||
|
||||
Reference in New Issue
Block a user