From 8302690995c23df7384b82de097e9781e5416325 Mon Sep 17 00:00:00 2001 From: Nicolargo Date: Fri, 15 Aug 2014 23:59:50 +0200 Subject: [PATCH 1/6] Add the process filter feature --- glances/core/glances_main.py | 4 +- glances/core/glances_processes.py | 50 +++++++++++- glances/core/glances_standalone.py | 4 + glances/outputs/glances_curses.py | 104 ++++++++++++++++++++---- glances/plugins/glances_help.py | 6 ++ glances/plugins/glances_processcount.py | 13 ++- 6 files changed, 158 insertions(+), 23 deletions(-) diff --git a/glances/core/glances_main.py b/glances/core/glances_main.py index c7b9b90a..b7c58eb7 100644 --- a/glances/core/glances_main.py +++ b/glances/core/glances_main.py @@ -111,6 +111,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 +143,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 diff --git a/glances/core/glances_processes.py b/glances/core/glances_processes.py index ef32e138..f41a2140 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 . +# 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,6 +96,37 @@ 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: + return self.get_process_filter_re().match(value) is None + def __get_process_stats(self, proc, mandatory_stats=True, standard_stats=True, @@ -237,9 +278,12 @@ 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) + if self.is_filtered(s['name']): + continue + 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_curses.py b/glances/outputs/glances_curses.py index 5784cf29..bd5a2058 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 @@ -136,6 +133,7 @@ class GlancesCurses(object): 'BOLD': A_BOLD, 'SORT': A_BOLD, 'OK': self.default_color2, + 'FILTER': self.ifCAREFUL_color2, 'TITLE': self.title_color, 'PROCESS': self.default_color2, 'STATUS': self.default_color2, @@ -158,6 +156,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 +175,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 +210,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 +479,41 @@ class GlancesCurses(object): self.history_tag = False self.reset_history_tag = False + # Display edit filter popup + if self.edit_filter: + new_filter = self.display_popup(_("Filter: "), + is_input=True, + input_value=glances_processes.get_process_filter()) + glances_processes.set_process_filter(new_filter) + 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=20, + 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 +526,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 +536,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 +707,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/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 ") From bb2d9d4cae59dd0423bc580bc82c84a4c03ec374 Mon Sep 17 00:00:00 2001 From: Nicolargo Date: Sat, 16 Aug 2014 14:28:46 +0200 Subject: [PATCH 2/6] Also filter on command line --- glances/core/glances_processes.py | 33 +++++++++++++++++-------------- glances/outputs/glances_curses.py | 4 ++-- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/glances/core/glances_processes.py b/glances/core/glances_processes.py index f41a2140..055b5435 100644 --- a/glances/core/glances_processes.py +++ b/glances/core/glances_processes.py @@ -125,6 +125,7 @@ class GlancesProcesses(object): # 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, @@ -133,10 +134,10 @@ class GlancesProcesses(object): 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... """ @@ -150,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] @@ -196,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() @@ -281,8 +282,10 @@ class GlancesProcesses(object): s = self.__get_process_stats(proc, mandatory_stats=True, standard_stats=self.get_max_processes() is None) - if self.is_filtered(s['name']): + # 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 diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index bd5a2058..786ecd36 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -481,7 +481,7 @@ class GlancesCurses(object): # Display edit filter popup if self.edit_filter: - new_filter = self.display_popup(_("Filter: "), + 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) @@ -493,7 +493,7 @@ class GlancesCurses(object): size_x=None, size_y=None, duration=3, is_input=False, - input_size=20, + input_size=30, input_value=None): """ If is_input is False: From 708c07a40a8c298dc9a5755d314d4fc0a56fbc57 Mon Sep 17 00:00:00 2001 From: Nicolargo Date: Sat, 16 Aug 2014 14:33:35 +0200 Subject: [PATCH 3/6] Also filter on command line --- glances/outputs/glances_curses.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index 786ecd36..e4dc0823 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -90,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 @@ -113,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 @@ -125,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 = { @@ -133,7 +136,7 @@ class GlancesCurses(object): 'BOLD': A_BOLD, 'SORT': A_BOLD, 'OK': self.default_color2, - 'FILTER': self.ifCAREFUL_color2, + 'FILTER': self.filter_color, 'TITLE': self.title_color, 'PROCESS': self.default_color2, 'STATUS': self.default_color2, From 32190fedee931f7988796978c1bb20edc413c15e Mon Sep 17 00:00:00 2001 From: Nicolargo Date: Sat, 16 Aug 2014 14:37:21 +0200 Subject: [PATCH 4/6] Chenge color for process filter --- glances/outputs/glances_bottle.py | 1 + glances/outputs/static/css/style.css | 3 +++ 2 files changed, 4 insertions(+) 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/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; From 5ec391474e8b6a84c7b84062f691540130cbe4c0 Mon Sep 17 00:00:00 2001 From: Nicolargo Date: Sat, 16 Aug 2014 15:05:42 +0200 Subject: [PATCH 5/6] Limit process filter to standalone mode --- glances/core/glances_main.py | 9 +++++++++ glances/outputs/glances_curses.py | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/glances/core/glances_main.py b/glances/core/glances_main.py index b7c58eb7..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 @@ -174,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/outputs/glances_curses.py b/glances/outputs/glances_curses.py index e4dc0823..fb4487b7 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -483,11 +483,14 @@ class GlancesCurses(object): self.reset_history_tag = False # Display edit filter popup - if self.edit_filter: + # 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 From 73b4ddf2645120e31d8002ff7ba3087ee1411647 Mon Sep 17 00:00:00 2001 From: Nicolargo Date: Sat, 16 Aug 2014 15:08:15 +0200 Subject: [PATCH 6/6] Update NEWS file --- NEWS | 5 +++++ 1 file changed, 5 insertions(+) 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 ) + 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.