From fa7fc5bdcdc84e8d05ed0a75151a36d1b8c8977b Mon Sep 17 00:00:00 2001 From: Ariel Otilibili Date: Sat, 14 Sep 2024 11:09:45 +0200 Subject: [PATCH 1/4] Refactorized alert plugin * closes #2416 * `msg_curse` calls `loop_over_alert` * `loop_over_alert` loops each alert with: 1. `add_new_line` 2. `add_start_time` 3. `add_duration` 4. `add_infos` 5. `add_min_mean_max` 6. `add_top_proc` Signed-off-by: Ariel Otilibili --- glances/plugins/alert/__init__.py | 125 +++++++++++++++++++----------- 1 file changed, 80 insertions(+), 45 deletions(-) diff --git a/glances/plugins/alert/__init__.py b/glances/plugins/alert/__init__.py index 9bc26816..b78c215d 100644 --- a/glances/plugins/alert/__init__.py +++ b/glances/plugins/alert/__init__.py @@ -9,6 +9,7 @@ """Alert plugin.""" from datetime import datetime +from functools import reduce from glances.events_list import glances_events @@ -123,58 +124,92 @@ class PluginModel(GlancesPluginModel): # Set the stats to the glances_events self.stats = glances_events.get() + def build_hdr_msg(self, ret): + def cond(elem): + return elem['end'] == -1 and 'global_msg' in elem + + global_message = [elem['global_msg'] for elem in self.stats if cond(elem)] + title = global_message[0] if len(global_message) > 0 else "EVENTS history" + + ret.append(self.curse_add_line(title, "TITLE")) + + return ret + + def add_new_line(self, ret, alert): + ret.append(self.curse_new_line()) + + return ret + + def add_start_time(self, ret, alert): + timezone = datetime.now().astimezone().tzinfo + alert_dt = datetime.fromtimestamp(alert['begin'], tz=timezone) + ret.append(self.curse_add_line(alert_dt.strftime("%Y-%m-%d %H:%M:%S(%z)"))) + + return ret + + def add_duration(self, ret, alert): + if alert['end'] > 0: + # If finished display duration + end = datetime.fromtimestamp(alert['end']) + begin = datetime.fromtimestamp(alert['begin']) + msg = f' ({end - begin})' + else: + msg = ' (ongoing)' + ret.append(self.curse_add_line(msg)) + ret.append(self.curse_add_line(" - ")) + + return ret + + def add_infos(self, ret, alert): + if alert['end'] > 0: + # If finished do not display status + msg = '{} on {}'.format(alert['state'], alert['type']) + ret.append(self.curse_add_line(msg)) + else: + msg = str(alert['type']) + ret.append(self.curse_add_line(msg, decoration=alert['state'])) + + return ret + + def add_min_mean_max(self, ret, alert): + if self.approx_equal(alert['min'], alert['max'], tolerance=0.1): + msg = ' ({:.1f})'.format(alert['avg']) + else: + msg = ' (Min:{:.1f} Mean:{:.1f} Max:{:.1f})'.format(alert['min'], alert['avg'], alert['max']) + ret.append(self.curse_add_line(msg)) + + return ret + + def add_top_proc(self, ret, alert): + top_process = ', '.join(alert['top']) + if top_process != '': + msg = f': {top_process}' + ret.append(self.curse_add_line(msg)) + + return ret + + def loop_over_alert(self, init, alert): + steps = [ + self.add_new_line, + self.add_start_time, + self.add_duration, + self.add_infos, + self.add_min_mean_max, + self.add_top_proc, + ] + + return reduce(lambda ret, step: step(ret, alert), steps, init) + def msg_curse(self, args=None, max_width=None): """Return the dict to display in the curse interface.""" # Init the return message - ret = [] + init = [] # Only process if display plugin enable... if not self.stats or self.is_disabled(): - return ret + return init - # Build the string message - # Header with the global message - global_message = [e['global_msg'] for e in self.stats if (e['end'] == -1 and 'global_msg' in e)] - if len(global_message) > 0: - ret.append(self.curse_add_line(global_message[0], "TITLE")) - else: - ret.append(self.curse_add_line("EVENTS history", "TITLE")) - # Loop over alerts - for alert in self.stats: - # New line - ret.append(self.curse_new_line()) - # Start - alert_dt = datetime.fromtimestamp(alert['begin'], tz=datetime.now().astimezone().tzinfo) - ret.append(self.curse_add_line(alert_dt.strftime("%Y-%m-%d %H:%M:%S(%z)"))) - # Duration - if alert['end'] > 0: - # If finished display duration - msg = ' ({})'.format(datetime.fromtimestamp(alert['end']) - datetime.fromtimestamp(alert['begin'])) - else: - msg = ' (ongoing)' - ret.append(self.curse_add_line(msg)) - ret.append(self.curse_add_line(" - ")) - # Infos - if alert['end'] > 0: - # If finished do not display status - msg = '{} on {}'.format(alert['state'], alert['type']) - ret.append(self.curse_add_line(msg)) - else: - msg = str(alert['type']) - ret.append(self.curse_add_line(msg, decoration=alert['state'])) - # Min / Mean / Max - if self.approx_equal(alert['min'], alert['max'], tolerance=0.1): - msg = ' ({:.1f})'.format(alert['avg']) - else: - msg = ' (Min:{:.1f} Mean:{:.1f} Max:{:.1f})'.format(alert['min'], alert['avg'], alert['max']) - ret.append(self.curse_add_line(msg)) - # Top processes - top_process = ', '.join(alert['top']) - if top_process != '': - msg = f': {top_process}' - ret.append(self.curse_add_line(msg)) - - return ret + return reduce(self.loop_over_alert, self.stats, self.build_hdr_msg(init)) def approx_equal(self, a, b, tolerance=0.0): """Compare a with b using the tolerance (if numerical).""" From 400013bb4a6a66fde26d9e5f4de1f93841e484b1 Mon Sep 17 00:00:00 2001 From: Ariel Otilibili Date: Mon, 16 Sep 2024 20:07:11 +0200 Subject: [PATCH 2/4] Revert "Refactorized `_GlancesCurses.display_plugin()`" * reverted commit 01823df917b3e51a3567698e35a9624e754f1092 * that commit introduced #2949. --- glances/outputs/glances_curses.py | 174 ++++++++++++------------------ 1 file changed, 70 insertions(+), 104 deletions(-) diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index 96c4e1fe..dab658d0 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -922,32 +922,6 @@ class _GlancesCurses: return None - def set_upper_left_pos(self, plugin_stats): - screen_x = self.term_window.getmaxyx()[1] - screen_y = self.term_window.getmaxyx()[0] - - if plugin_stats['align'] == 'right': - # Right align (last column) - display_x = screen_x - self.get_stats_display_width(plugin_stats) - else: - display_x = self.column - if plugin_stats['align'] == 'bottom': - # Bottom (last line) - display_y = screen_y - self.get_stats_display_height(plugin_stats) - else: - display_y = self.line - - return display_y, display_x - - def check_opt_and_add(self, display_optional, display_additional): - def neither_optional_nor_additional(m): - has_optional = not display_optional and m['optional'] - has_additional = not display_additional and m['additional'] - - return any([has_optional, has_additional]) - - return neither_optional_nor_additional - def display_plugin(self, plugin_stats, display_optional=True, display_additional=True, max_y=65535, add_space=0): """Display the plugin_stats on the screen. @@ -960,23 +934,81 @@ class _GlancesCurses: # Exit if: # - the plugin_stats message is empty # - the display tag = False - conditions = [plugin_stats is None, not plugin_stats['msgdict'], not plugin_stats['display']] - if any(conditions): + if plugin_stats is None or not plugin_stats['msgdict'] or not plugin_stats['display']: # Exit return 0 + # Get the screen size + screen_x = self.term_window.getmaxyx()[1] + screen_y = self.term_window.getmaxyx()[0] + # Set the upper/left position of the message - display_y, display_x = self.set_upper_left_pos(plugin_stats) + if plugin_stats['align'] == 'right': + # Right align (last column) + display_x = screen_x - self.get_stats_display_width(plugin_stats) + else: + display_x = self.column + if plugin_stats['align'] == 'bottom': + # Bottom (last line) + display_y = screen_y - self.get_stats_display_height(plugin_stats) + else: + display_y = self.line - helper = { - 'goto next and ret first col': self.goto_next_and_ret_first_col(display_x), - 'neither opt nor add?': self.check_opt_and_add(display_optional, display_additional), - 'x overbound?': self.x_overbound, - 'y overbound?': self.y_overbound(max_y), - } - - init = {'x': display_x, 'x max': display_x, 'y': display_y} - y, x, x_max = self.display_msg(plugin_stats, init, helper) + # Display + x = display_x + x_max = x + y = display_y + for m in plugin_stats['msgdict']: + # New line + try: + if m['msg'].startswith('\n'): + # Go to the next line + y += 1 + # Return to the first column + x = display_x + continue + except Exception: + # Avoid exception (see issue #1692) + pass + # Do not display outside the screen + if x < 0: + continue + if not m['splittable'] and (x + len(m['msg']) > screen_x): + continue + if y < 0 or (y + 1 > screen_y) or (y > max_y): + break + # If display_optional = False do not display optional stats + if not display_optional and m['optional']: + continue + # If display_additional = False do not display additional stats + if not display_additional and m['additional']: + continue + # Is it possible to display the stat with the current screen size + # !!! Crash if not try/except... Why ??? + try: + self.term_window.addnstr( + y, + x, + m['msg'], + # Do not display outside the screen + screen_x - x, + self.colors_list[m['decoration']], + ) + except Exception: + pass + else: + # New column + # Python 2: we need to decode to get real screen size because + # UTF-8 special tree chars occupy several bytes. + # Python 3: strings are strings and bytes are bytes, all is + # good. + try: + x += len(u(m['msg'])) + except UnicodeDecodeError: + # Quick and dirty hack for issue #745 + pass + if x > x_max: + x_max = x # Compute the next Glances column/line position self.next_column = max(self.next_column, x_max + self.space_between_column) @@ -986,72 +1018,6 @@ class _GlancesCurses: self.next_line += add_space return None - def y_overbound(self, max_y): - screen_y = self.term_window.getmaxyx()[0] - - return lambda y: y < 0 or (y + 1 > screen_y) or (y > max_y) - - def x_overbound(self, m, x): - screen_x = self.term_window.getmaxyx()[1] - - return x < 0 or not m['splittable'] and (x + len(m['msg']) > screen_x) - - def goto_next_and_ret_first_col(self, display_x): - return lambda y: (y + 1, display_x) - - def display_msg(self, plugin_stats, init, helper): - y, x, x_max = init['y'], init['x'], init['x max'] - for m in plugin_stats['msgdict']: - # New line - try: - if m['msg'].startswith('\n'): - y, x = helper['goto next and ret first col'](y) - continue - except Exception: - # Avoid exception (see issue #1692) - pass - if helper['x overbound?'](m, x) or helper['neither opt nor add?'](m): - continue - if helper['y overbound?'](y): - break - x, x_max = self.display_stats_with_current_size(m, y, x, x_max) - - return y, x, x_max - - def display_stats_with_current_size(self, m, y, x, x_max): - # Is it possible to display the stat with the current screen size - # !!! Crash if not try/except... Why ??? - screen_x = self.term_window.getmaxyx()[1] - try: - self.term_window.addnstr( - y, - x, - m['msg'], - # Do not display outside the screen - screen_x - x, - self.colors_list[m['decoration']], - ) - except Exception: - pass - else: - return self.add_new_colum(m, x, x_max) - - def add_new_colum(self, m, x, x_max): - # New column - # Python 2: we need to decode to get real screen size because - # UTF-8 special tree chars occupy several bytes. - # Python 3: strings are strings and bytes are bytes, all is - # good. - try: - x += len(u(m['msg'])) - except UnicodeDecodeError: - # Quick and dirty hack for issue #745 - pass - if x > x_max: - x_max = x - - return x, x_max - def clear(self): """Erase the content of the screen. The difference is that clear() also calls clearok(). clearok() From 4bcbc12ceae3e5d36b532d10397ae6b1ad4e5914 Mon Sep 17 00:00:00 2001 From: Ariel Otilibili Date: Tue, 17 Sep 2024 17:16:39 +0200 Subject: [PATCH 3/4] Refactorized `_GlancesCurses.display_plugin()`, version 2 Part of #2801. Broken down using: 1. `setup_upper_left_pos` 2. `get_next_x_and_x_max` 3. `display_stats_with_current_size` 4. `display_stats` Signed-off-by: Ariel Otilibili --- glances/outputs/glances_curses.py | 157 +++++++++++++++++------------- 1 file changed, 91 insertions(+), 66 deletions(-) diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index dab658d0..89a14b65 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -922,6 +922,85 @@ class _GlancesCurses: return None + def setup_upper_left_pos(self, plugin_stats): + screen_y, screen_x = self.term_window.getmaxyx() + + if plugin_stats['align'] == 'right': + # Right align (last column) + display_x = screen_x - self.get_stats_display_width(plugin_stats) + else: + display_x = self.column + + if plugin_stats['align'] == 'bottom': + # Bottom (last line) + display_y = screen_y - self.get_stats_display_height(plugin_stats) + else: + display_y = self.line + + return display_y, display_x + + def get_next_x_and_x_max(self, m, x, x_max): + # New column + # Python 2: we need to decode to get real screen size because + # UTF-8 special tree chars occupy several bytes. + # Python 3: strings are strings and bytes are bytes, all is + # good. + try: + x += len(u(m['msg'])) + except UnicodeDecodeError: + # Quick and dirty hack for issue #745 + pass + if x > x_max: + x_max = x + + return x, x_max + + def display_stats_with_current_size(self, m, y, x): + screen_x = self.term_window.getmaxyx()[1] + self.term_window.addnstr( + y, + x, + m['msg'], + # Do not display outside the screen + screen_x - x, + self.colors_list[m['decoration']], + ) + + def display_stats(self, plugin_stats, init, helper): + y, x, x_max = init + for m in plugin_stats['msgdict']: + # New line + try: + if m['msg'].startswith('\n'): + y, x = helper['goto next, add first col'](y, x) + continue + except Exception: + # Avoid exception (see issue #1692) + pass + # Do not display outside the screen + if x < 0: + continue + if helper['x overbound?'](m, x): + continue + if helper['y overbound?'](y): + break + # If display_optional = False do not display optional stats + if helper['display optional?'](m): + continue + # If display_additional = False do not display additional stats + if helper['display additional?'](m): + continue + # Is it possible to display the stat with the current screen size + # !!! Crash if not try/except... Why ??? + try: + self.display_stats_with_current_size(m, y, x) + except Exception: + pass + else: + x, x_max = self.get_next_x_and_x_max(m, x, x_max) + + return y, x, x_max + def display_plugin(self, plugin_stats, display_optional=True, display_additional=True, max_y=65535, add_space=0): """Display the plugin_stats on the screen. @@ -939,76 +1018,22 @@ class _GlancesCurses: return 0 # Get the screen size - screen_x = self.term_window.getmaxyx()[1] - screen_y = self.term_window.getmaxyx()[0] + screen_y, screen_x = self.term_window.getmaxyx() # Set the upper/left position of the message - if plugin_stats['align'] == 'right': - # Right align (last column) - display_x = screen_x - self.get_stats_display_width(plugin_stats) - else: - display_x = self.column - if plugin_stats['align'] == 'bottom': - # Bottom (last line) - display_y = screen_y - self.get_stats_display_height(plugin_stats) - else: - display_y = self.line + display_y, display_x = self.setup_upper_left_pos(plugin_stats) + + helper = { + 'goto next, add first col': lambda y, x: (y + 1, display_x), + 'x overbound?': lambda m, x: not m['splittable'] and (x + len(m['msg']) > screen_x), + 'y overbound?': lambda y: y < 0 or (y + 1 > screen_y) or (y > max_y), + 'display optional?': lambda m: not display_optional and m['optional'], + 'display additional?': lambda m: not display_additional and m['additional'], + } # Display - x = display_x - x_max = x - y = display_y - for m in plugin_stats['msgdict']: - # New line - try: - if m['msg'].startswith('\n'): - # Go to the next line - y += 1 - # Return to the first column - x = display_x - continue - except Exception: - # Avoid exception (see issue #1692) - pass - # Do not display outside the screen - if x < 0: - continue - if not m['splittable'] and (x + len(m['msg']) > screen_x): - continue - if y < 0 or (y + 1 > screen_y) or (y > max_y): - break - # If display_optional = False do not display optional stats - if not display_optional and m['optional']: - continue - # If display_additional = False do not display additional stats - if not display_additional and m['additional']: - continue - # Is it possible to display the stat with the current screen size - # !!! Crash if not try/except... Why ??? - try: - self.term_window.addnstr( - y, - x, - m['msg'], - # Do not display outside the screen - screen_x - x, - self.colors_list[m['decoration']], - ) - except Exception: - pass - else: - # New column - # Python 2: we need to decode to get real screen size because - # UTF-8 special tree chars occupy several bytes. - # Python 3: strings are strings and bytes are bytes, all is - # good. - try: - x += len(u(m['msg'])) - except UnicodeDecodeError: - # Quick and dirty hack for issue #745 - pass - if x > x_max: - x_max = x + init = display_y, display_x, display_x + y, x, x_max = self.display_stats(plugin_stats, init, helper) # Compute the next Glances column/line position self.next_column = max(self.next_column, x_max + self.space_between_column) From dec419f443162be3e23ffef54ea09710849d169b Mon Sep 17 00:00:00 2001 From: Ariel Otilibili Date: Mon, 23 Sep 2024 22:42:21 +0200 Subject: [PATCH 4/4] Refactored `webui*` rules * part of #2906 * output is unchanged. ``` $ git log --oneline -n1 --pretty=short commit f2f97e2f (HEAD -> makefile) Author: Ariel Otilibili Refactored `webui*` rules $ make webui -n >> /tmp/makefile; echo $? 0 $ make webui-audit -n >> /tmp/makefile; echo $? 0 $ make webui-audit-fix -n >> /tmp/makefile; echo $? 0 $ git switch develop Switched to branch 'develop' Your branch is up to date with 'origin/develop'. $ git log --oneline -n1 --pretty=short commit 24c87253 (HEAD -> develop, origin/develop, origin/HEAD) Merge: 42af55b3 fa7fc5bd Author: Nicolas Hennion Merge pull request #2941 from ariel-anieli/refactor-alert $ make webui -n >> /tmp/develop; echo $? 0 $ make webui-audit -n >> /tmp/develop; echo $? 0 $ make webui-audit-fix -n >> /tmp/develop; echo $? 0 $ diff /tmp/develop /tmp/makefile; echo $? 0 ``` Signed-off-by: Ariel Otilibili --- Makefile | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 94ca286d..1ed64e35 100644 --- a/Makefile +++ b/Makefile @@ -203,14 +203,16 @@ install: ## Open a Web Browser to the installation procedure # Follow ./glances/outputs/static/README.md for more information # =================================================================== +webui webui%: DIR = glances/outputs/static/ + webui: ## Build the Web UI - cd glances/outputs/static/ && npm ci && npm run build + cd $(DIR) && npm ci && npm run build webui-audit: ## Audit the Web UI - cd glances/outputs/static/ && npm audit + cd $(DIR) && npm audit webui-audit-fix: ## Fix audit the Web UI - cd glances/outputs/static/ && npm audit fix && npm ci && npm run build + cd $(DIR) && npm audit fix && npm ci && npm run build # =================================================================== # Packaging