Merge branch 'feature/issue405' into develop (#issue405)

Process filter patern feature is now available.
This commit is contained in:
Nicolargo
2014-08-16 15:08:31 +02:00
9 changed files with 199 additions and 37 deletions

5
NEWS
View File

@@ -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.

View File

@@ -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):

View File

@@ -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

View File

@@ -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()

View File

@@ -64,6 +64,7 @@ class GlancesBottle(object):
'BOLD': 'bold',
'SORT': 'sort',
'OK': 'ok',
'FILTER': 'filter',
'TITLE': 'title',
'CAREFUL': 'careful',
'WARNING': 'warning',

View File

@@ -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)

View File

@@ -61,6 +61,9 @@ div#newline{
#ok {
color: green;
}
#filter {
color: cyan;
}
#ok_log {
background-color: green;
color: white;

View File

@@ -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

View File

@@ -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 ")