summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNicolargo <nicolas@nicolargo.com>2014-08-16 15:08:31 +0200
committerNicolargo <nicolas@nicolargo.com>2014-08-16 15:08:31 +0200
commit849052ec55fc0a76124cf19de8d64c361fa9d160 (patch)
treeb0e4e3bc4e9f940440313bb32472a000dc15626a
parent37edbfc5a18f2bf424b1f34f2d158d03f18983c0 (diff)
parent73b4ddf2645120e31d8002ff7ba3087ee1411647 (diff)
Merge branch 'feature/issue405' into develop (#issue405)
Process filter patern feature is now available.
-rw-r--r--NEWS5
-rw-r--r--glances/core/glances_main.py13
-rw-r--r--glances/core/glances_processes.py81
-rw-r--r--glances/core/glances_standalone.py4
-rw-r--r--glances/outputs/glances_bottle.py1
-rw-r--r--glances/outputs/glances_curses.py110
-rw-r--r--glances/outputs/static/css/style.css3
-rw-r--r--glances/plugins/glances_help.py6
-rw-r--r--glances/plugins/glances_processcount.py13
9 files changed, 199 insertions, 37 deletions
diff --git a/NEWS b/NEWS
index 05e817c9..d14db909 100644
--- a/NEWS
+++ b/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.
diff --git a/glances/core/glances_main.py b/glances/core/glances_main.py
index c7b9b90a..e8928b37 100644
--- a/glances/core/glances_main.py
+++ b/glances/core/glances_main.py
@@ -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):
diff --git a/glances/core/glances_processes.py b/glances/core/glances_processes.py
index ef32e138..055b5435 100644
--- a/glances/core/glances_processes.py
+++ b/glances/core/glances_processes.py
@@ -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
diff --git a/glances/core/glances_standalone.py b/glances/core/glances_standalone.py
index f74f120d..a6586f6b 100644
--- a/glances/core/glances_standalone.py
+++ b/glances/core/glances_standalone.py
@@ -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()
diff --git a/glances/outputs/glances_bottle.py b/glances/outputs/glances_bottle.py
index a79dcb30..da95af15 100644
--- a/glances/outputs/glances_bottle.py
+++ b/glances/outputs/glances_bottle.py
@@ -64,6 +64,7 @@ class GlancesBottle(object):
'BOLD': 'bold',
'SORT': 'sort',
'OK': 'ok',
+ 'FILTER': 'filter',
'TITLE': 'title',
'CAREFUL': 'careful',
'WARNING': 'warning',
diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py
index 5784cf29..fb4487b7 100644
--- a/glances/outputs/glances_curses.py
+++ b/glances/outputs/glances_curses.py
@@ -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) \ No newline at end of file
diff --git a/glances/outputs/static/css/style.css b/glances/outputs/static/css/style.css
index 49e45f59..59980f2b 100644
--- a/glances/outputs/static/css/style.css
+++ b/glances/outputs/static/css/style.css
@@ -61,6 +61,9 @@ div#newline{
#ok {
color: green;
}
+#filter {
+ color: cyan;
+}
#ok_log {
background-color: green;
color: white;
diff --git a/glances/plugins/glances_help.py b/glances/plugins/glances_help.py
index 9ceff591..9ae48416 100644
--- a/glances/plugins/glances_help.py
+++ b/glances/plugins/glances_help.py
@@ -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
diff --git a/glances/plugins/glances_processcount.py b/glances/plugins/glances_processcount.py
index 95e507be..b171258a 100644
--- a/glances/plugins/glances_processcount.py
+++ b/glances/plugins/glances_processcount.py
@@ -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 ")