1 | # -*- coding: utf-8 -*- |
||
2 | # |
||
3 | # This file is part of Glances. |
||
4 | # |
||
5 | # Copyright (C) 2019 Nicolargo <[email protected]> |
||
6 | # |
||
7 | # Glances is free software; you can redistribute it and/or modify |
||
8 | # it under the terms of the GNU Lesser General Public License as published by |
||
9 | # the Free Software Foundation, either version 3 of the License, or |
||
10 | # (at your option) any later version. |
||
11 | # |
||
12 | # Glances is distributed in the hope that it will be useful, |
||
13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
15 | # GNU Lesser General Public License for more details. |
||
16 | # |
||
17 | # You should have received a copy of the GNU Lesser General Public License |
||
18 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
||
19 | |||
20 | """Curses interface class.""" |
||
21 | from __future__ import unicode_literals |
||
22 | |||
23 | import re |
||
24 | import sys |
||
25 | |||
26 | from glances.compat import to_ascii, nativestr, b, u, itervalues |
||
27 | from glances.globals import MACOS, WINDOWS |
||
28 | from glances.logger import logger |
||
29 | from glances.events import glances_events |
||
30 | from glances.processes import glances_processes |
||
31 | from glances.timer import Timer |
||
32 | |||
33 | # Import curses library for "normal" operating system |
||
34 | if not WINDOWS: |
||
35 | try: |
||
36 | import curses |
||
37 | import curses.panel |
||
38 | from curses.textpad import Textbox |
||
39 | except ImportError: |
||
40 | logger.critical("Curses module not found. Glances cannot start in standalone mode.") |
||
41 | sys.exit(1) |
||
42 | |||
43 | |||
44 | class _GlancesCurses(object): |
||
45 | |||
46 | """This class manages the curses display (and key pressed). |
||
47 | |||
48 | Note: It is a private class, use GlancesCursesClient or GlancesCursesBrowser. |
||
49 | """ |
||
50 | |||
51 | _hotkeys = { |
||
52 | '0': {'switch': 'disable_irix'}, |
||
53 | '1': {'switch': 'percpu'}, |
||
54 | '2': {'switch': 'disable_left_sidebar'}, |
||
55 | '3': {'switch': 'disable_quicklook'}, |
||
56 | '6': {'switch': 'meangpu'}, |
||
57 | '/': {'switch': 'process_short_name'}, |
||
58 | 'A': {'switch': 'disable_amps'}, |
||
59 | 'b': {'switch': 'byte'}, |
||
60 | 'B': {'switch': 'diskio_iops'}, |
||
61 | 'C': {'switch': 'disable_cloud'}, |
||
62 | 'D': {'switch': 'disable_docker'}, |
||
63 | 'd': {'switch': 'disable_diskio'}, |
||
64 | 'F': {'switch': 'fs_free_space'}, |
||
65 | 'g': {'switch': 'generate_graph'}, |
||
66 | 'G': {'switch': 'disable_gpu'}, |
||
67 | 'h': {'switch': 'help_tag'}, |
||
68 | 'I': {'switch': 'disable_ip'}, |
||
69 | 'l': {'switch': 'disable_alert'}, |
||
70 | 'M': {'switch': 'reset_minmax_tag'}, |
||
71 | 'n': {'switch': 'disable_network'}, |
||
72 | 'N': {'switch': 'disable_now'}, |
||
73 | 'P': {'switch': 'disable_ports'}, |
||
74 | 'Q': {'switch': 'enable_irq'}, |
||
75 | 'R': {'switch': 'disable_raid'}, |
||
76 | 's': {'switch': 'disable_sensors'}, |
||
77 | 'S': {'switch': 'sparkline'}, |
||
78 | 'T': {'switch': 'network_sum'}, |
||
79 | 'U': {'switch': 'network_cumul'}, |
||
80 | 'W': {'switch': 'disable_wifi'}, |
||
81 | # Processes sort hotkeys |
||
82 | 'a': {'auto_sort': True, 'sort_key': 'cpu_percent'}, |
||
83 | 'c': {'auto_sort': False, 'sort_key': 'cpu_percent'}, |
||
84 | 'i': {'auto_sort': False, 'sort_key': 'io_counters'}, |
||
85 | 'm': {'auto_sort': False, 'sort_key': 'memory_percent'}, |
||
86 | 'p': {'auto_sort': False, 'sort_key': 'name'}, |
||
87 | 't': {'auto_sort': False, 'sort_key': 'cpu_times'}, |
||
88 | 'u': {'auto_sort': False, 'sort_key': 'username'}, |
||
89 | } |
||
90 | |||
91 | _sort_loop = ['cpu_percent', 'memory_percent', 'username', |
||
92 | 'cpu_times', 'io_counters', 'name'] |
||
93 | |||
94 | # Define top menu |
||
95 | _top = ['quicklook', 'cpu', 'percpu', 'gpu', 'mem', 'memswap', 'load'] |
||
96 | _quicklook_max_width = 68 |
||
97 | |||
98 | # Define left sidebar |
||
99 | _left_sidebar = ['network', 'wifi', 'ports', 'diskio', 'fs', |
||
100 | 'irq', 'folders', 'raid', 'sensors', 'now'] |
||
101 | _left_sidebar_min_width = 23 |
||
102 | _left_sidebar_max_width = 34 |
||
103 | |||
104 | # Define right sidebar |
||
105 | _right_sidebar = ['docker', 'processcount', 'amps', 'processlist', 'alert'] |
||
106 | |||
107 | def __init__(self, config=None, args=None): |
||
108 | # Init |
||
109 | self.config = config |
||
110 | self.args = args |
||
111 | |||
112 | # Init windows positions |
||
113 | self.term_w = 80 |
||
114 | self.term_h = 24 |
||
115 | |||
116 | # Space between stats |
||
117 | self.space_between_column = 3 |
||
118 | self.space_between_line = 2 |
||
119 | |||
120 | # Init the curses screen |
||
121 | self.screen = curses.initscr() |
||
122 | if not self.screen: |
||
123 | logger.critical("Cannot init the curses library.\n") |
||
124 | sys.exit(1) |
||
125 | |||
126 | # Load the 'outputs' section of the configuration file |
||
127 | # - Init the theme (default is black) |
||
128 | self.theme = {'name': 'black'} |
||
129 | |||
130 | # Load configuration file |
||
131 | self.load_config(config) |
||
132 | |||
133 | # Init cursor |
||
134 | self._init_cursor() |
||
135 | |||
136 | # Init the colors |
||
137 | self._init_colors() |
||
138 | |||
139 | # Init main window |
||
140 | self.term_window = self.screen.subwin(0, 0) |
||
141 | |||
142 | # Init edit filter tag |
||
143 | self.edit_filter = False |
||
144 | |||
145 | # Init the process min/max reset |
||
146 | self.args.reset_minmax_tag = False |
||
147 | |||
148 | # Catch key pressed with non blocking mode |
||
149 | self.term_window.keypad(1) |
||
150 | self.term_window.nodelay(1) |
||
151 | self.pressedkey = -1 |
||
152 | |||
153 | # History tag |
||
154 | self._init_history() |
||
155 | |||
156 | def load_config(self, config): |
||
157 | """Load the outputs section of the configuration file.""" |
||
158 | # Load the theme |
||
159 | if config is not None and config.has_section('outputs'): |
||
160 | logger.debug('Read the outputs section in the configuration file') |
||
161 | self.theme['name'] = config.get_value('outputs', 'curse_theme', default='black') |
||
162 | logger.debug('Theme for the curse interface: {}'.format(self.theme['name'])) |
||
163 | |||
164 | def is_theme(self, name): |
||
165 | """Return True if the theme *name* should be used.""" |
||
166 | return getattr(self.args, 'theme_' + name) or self.theme['name'] == name |
||
167 | |||
168 | def _init_history(self): |
||
169 | """Init the history option.""" |
||
170 | |||
171 | self.reset_history_tag = False |
||
172 | |||
173 | def _init_cursor(self): |
||
174 | """Init cursors.""" |
||
175 | |||
176 | if hasattr(curses, 'noecho'): |
||
177 | curses.noecho() |
||
178 | if hasattr(curses, 'cbreak'): |
||
179 | curses.cbreak() |
||
180 | self.set_cursor(0) |
||
181 | |||
182 | def _init_colors(self): |
||
183 | """Init the Curses color layout.""" |
||
184 | |||
185 | # Set curses options |
||
186 | try: |
||
187 | if hasattr(curses, 'start_color'): |
||
188 | curses.start_color() |
||
189 | if hasattr(curses, 'use_default_colors'): |
||
190 | curses.use_default_colors() |
||
191 | except Exception as e: |
||
192 | logger.warning('Error initializing terminal color ({})'.format(e)) |
||
193 | |||
194 | # Init colors |
||
195 | if self.args.disable_bold: |
||
196 | A_BOLD = 0 |
||
197 | self.args.disable_bg = True |
||
198 | else: |
||
199 | A_BOLD = curses.A_BOLD |
||
200 | |||
201 | self.title_color = A_BOLD |
||
202 | self.title_underline_color = A_BOLD | curses.A_UNDERLINE |
||
203 | self.help_color = A_BOLD |
||
204 | |||
205 | if curses.has_colors(): |
||
206 | # The screen is compatible with a colored design |
||
207 | if self.is_theme('white'): |
||
208 | # White theme: black ==> white |
||
209 | curses.init_pair(1, curses.COLOR_BLACK, -1) |
||
0 ignored issues
–
show
|
|||
210 | else: |
||
211 | curses.init_pair(1, curses.COLOR_WHITE, -1) |
||
0 ignored issues
–
show
|
|||
212 | if self.args.disable_bg: |
||
213 | curses.init_pair(2, curses.COLOR_RED, -1) |
||
0 ignored issues
–
show
|
|||
214 | curses.init_pair(3, curses.COLOR_GREEN, -1) |
||
0 ignored issues
–
show
|
|||
215 | curses.init_pair(4, curses.COLOR_BLUE, -1) |
||
216 | curses.init_pair(5, curses.COLOR_MAGENTA, -1) |
||
217 | else: |
||
218 | curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_RED) |
||
219 | curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_GREEN) |
||
220 | curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_BLUE) |
||
221 | curses.init_pair(5, curses.COLOR_WHITE, curses.COLOR_MAGENTA) |
||
222 | curses.init_pair(6, curses.COLOR_RED, -1) |
||
223 | curses.init_pair(7, curses.COLOR_GREEN, -1) |
||
224 | curses.init_pair(8, curses.COLOR_BLUE, -1) |
||
225 | |||
226 | # Colors text styles |
||
227 | if curses.COLOR_PAIRS > 8: |
||
228 | try: |
||
229 | curses.init_pair(9, curses.COLOR_MAGENTA, -1) |
||
230 | except Exception: |
||
231 | if self.is_theme('white'): |
||
232 | curses.init_pair(9, curses.COLOR_BLACK, -1) |
||
233 | else: |
||
234 | curses.init_pair(9, curses.COLOR_WHITE, -1) |
||
235 | try: |
||
236 | curses.init_pair(10, curses.COLOR_CYAN, -1) |
||
237 | except Exception: |
||
238 | if self.is_theme('white'): |
||
239 | curses.init_pair(10, curses.COLOR_BLACK, -1) |
||
240 | else: |
||
241 | curses.init_pair(10, curses.COLOR_WHITE, -1) |
||
242 | |||
243 | self.ifWARNING_color2 = curses.color_pair(9) | A_BOLD |
||
244 | self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD |
||
245 | self.filter_color = curses.color_pair(10) | A_BOLD |
||
246 | |||
247 | self.no_color = curses.color_pair(1) |
||
248 | self.default_color = curses.color_pair(3) | A_BOLD |
||
249 | self.nice_color = curses.color_pair(9) |
||
250 | self.cpu_time_color = curses.color_pair(9) |
||
251 | self.ifCAREFUL_color = curses.color_pair(4) | A_BOLD |
||
252 | self.ifWARNING_color = curses.color_pair(5) | A_BOLD |
||
253 | self.ifCRITICAL_color = curses.color_pair(2) | A_BOLD |
||
254 | self.default_color2 = curses.color_pair(7) |
||
255 | self.ifCAREFUL_color2 = curses.color_pair(8) | A_BOLD |
||
256 | |||
257 | else: |
||
258 | # The screen is NOT compatible with a colored design |
||
259 | # switch to B&W text styles |
||
260 | self.no_color = curses.A_NORMAL |
||
261 | self.default_color = curses.A_NORMAL |
||
262 | self.nice_color = A_BOLD |
||
263 | self.cpu_time_color = A_BOLD |
||
264 | self.ifCAREFUL_color = curses.A_UNDERLINE |
||
265 | self.ifWARNING_color = A_BOLD |
||
266 | self.ifCRITICAL_color = curses.A_REVERSE |
||
267 | self.default_color2 = curses.A_NORMAL |
||
268 | self.ifCAREFUL_color2 = curses.A_UNDERLINE |
||
269 | self.ifWARNING_color2 = A_BOLD |
||
270 | self.ifCRITICAL_color2 = curses.A_REVERSE |
||
271 | self.filter_color = A_BOLD |
||
272 | |||
273 | # Define the colors list (hash table) for stats |
||
274 | self.colors_list = { |
||
275 | 'DEFAULT': self.no_color, |
||
276 | 'UNDERLINE': curses.A_UNDERLINE, |
||
277 | 'BOLD': A_BOLD, |
||
278 | 'SORT': A_BOLD, |
||
279 | 'OK': self.default_color2, |
||
280 | 'MAX': self.default_color2 | curses.A_BOLD, |
||
281 | 'FILTER': self.filter_color, |
||
282 | 'TITLE': self.title_color, |
||
283 | 'PROCESS': self.default_color2, |
||
284 | 'STATUS': self.default_color2, |
||
285 | 'NICE': self.nice_color, |
||
286 | 'CPU_TIME': self.cpu_time_color, |
||
287 | 'CAREFUL': self.ifCAREFUL_color2, |
||
288 | 'WARNING': self.ifWARNING_color2, |
||
289 | 'CRITICAL': self.ifCRITICAL_color2, |
||
290 | 'OK_LOG': self.default_color, |
||
291 | 'CAREFUL_LOG': self.ifCAREFUL_color, |
||
292 | 'WARNING_LOG': self.ifWARNING_color, |
||
293 | 'CRITICAL_LOG': self.ifCRITICAL_color, |
||
294 | 'PASSWORD': curses.A_PROTECT |
||
295 | } |
||
296 | |||
297 | def set_cursor(self, value): |
||
298 | """Configure the curse cursor apparence. |
||
299 | |||
300 | 0: invisible |
||
301 | 1: visible |
||
302 | 2: very visible |
||
303 | """ |
||
304 | if hasattr(curses, 'curs_set'): |
||
305 | try: |
||
306 | curses.curs_set(value) |
||
307 | except Exception: |
||
308 | pass |
||
309 | |||
310 | # def get_key(self, window): |
||
311 | # # Catch ESC key AND numlock key (issue #163) |
||
312 | # keycode = [0, 0] |
||
313 | # keycode[0] = window.getch() |
||
314 | # keycode[1] = window.getch() |
||
315 | # |
||
316 | # if keycode != [-1, -1]: |
||
317 | # logger.debug("Keypressed (code: %s)" % keycode) |
||
318 | # |
||
319 | # if keycode[0] == 27 and keycode[1] != -1: |
||
320 | # # Do not escape on specials keys |
||
321 | # return -1 |
||
322 | # else: |
||
323 | # return keycode[0] |
||
324 | |||
325 | def get_key(self, window): |
||
326 | # @TODO: Check issue #163 |
||
327 | ret = window.getch() |
||
328 | logger.debug("Keypressed (code: %s)" % ret) |
||
329 | return ret |
||
330 | |||
331 | def __catch_key(self, return_to_browser=False): |
||
332 | # Catch the pressed key |
||
333 | self.pressedkey = self.get_key(self.term_window) |
||
334 | |||
335 | # Actions (available in the global hotkey dict)... |
||
336 | for hotkey in self._hotkeys: |
||
337 | if self.pressedkey == ord(hotkey) and 'switch' in self._hotkeys[hotkey]: |
||
338 | setattr(self.args, |
||
339 | self._hotkeys[hotkey]['switch'], |
||
340 | not getattr(self.args, |
||
341 | self._hotkeys[hotkey]['switch'])) |
||
342 | if self.pressedkey == ord(hotkey) and 'auto_sort' in self._hotkeys[hotkey]: |
||
343 | setattr(glances_processes, |
||
344 | 'auto_sort', |
||
345 | self._hotkeys[hotkey]['auto_sort']) |
||
346 | if self.pressedkey == ord(hotkey) and 'sort_key' in self._hotkeys[hotkey]: |
||
347 | setattr(glances_processes, |
||
348 | 'sort_key', |
||
349 | self._hotkeys[hotkey]['sort_key']) |
||
350 | |||
351 | # Other actions... |
||
352 | if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'): |
||
353 | # 'ESC'|'q' > Quit |
||
354 | if return_to_browser: |
||
355 | logger.info("Stop Glances client and return to the browser") |
||
356 | else: |
||
357 | logger.info("Stop Glances (keypressed: {})".format(self.pressedkey)) |
||
358 | elif self.pressedkey == ord('\n'): |
||
359 | # 'ENTER' > Edit the process filter |
||
360 | self.edit_filter = not self.edit_filter |
||
361 | elif self.pressedkey == ord('4'): |
||
362 | self.args.full_quicklook = not self.args.full_quicklook |
||
363 | if self.args.full_quicklook: |
||
364 | self.enable_fullquicklook() |
||
365 | else: |
||
366 | self.disable_fullquicklook() |
||
367 | elif self.pressedkey == ord('5'): |
||
368 | self.args.disable_top = not self.args.disable_top |
||
369 | if self.args.disable_top: |
||
370 | self.disable_top() |
||
371 | else: |
||
372 | self.enable_top() |
||
373 | elif self.pressedkey == ord('e'): |
||
374 | # 'e' > Enable/Disable process extended |
||
375 | self.args.enable_process_extended = not self.args.enable_process_extended |
||
376 | if not self.args.enable_process_extended: |
||
377 | glances_processes.disable_extended() |
||
378 | else: |
||
379 | glances_processes.enable_extended() |
||
380 | elif self.pressedkey == ord('E'): |
||
381 | # 'E' > Erase the process filter |
||
382 | glances_processes.process_filter = None |
||
383 | elif self.pressedkey == ord('f'): |
||
384 | # 'f' > Show/hide fs / folder stats |
||
385 | self.args.disable_fs = not self.args.disable_fs |
||
386 | self.args.disable_folders = not self.args.disable_folders |
||
387 | elif self.pressedkey == ord('w'): |
||
388 | # 'w' > Delete finished warning logs |
||
389 | glances_events.clean() |
||
390 | elif self.pressedkey == ord('x'): |
||
391 | # 'x' > Delete finished warning and critical logs |
||
392 | glances_events.clean(critical=True) |
||
393 | elif self.pressedkey == ord('z'): |
||
394 | # 'z' > Enable or disable processes |
||
395 | self.args.disable_process = not self.args.disable_process |
||
396 | if self.args.disable_process: |
||
397 | glances_processes.disable() |
||
398 | else: |
||
399 | glances_processes.enable() |
||
400 | elif self.pressedkey == curses.KEY_LEFT: |
||
401 | # "<" (left arrow) navigation through process sort |
||
402 | setattr(glances_processes, 'auto_sort', False) |
||
403 | next_sort = (self.loop_position() - 1) % len(self._sort_loop) |
||
404 | glances_processes.sort_key = self._sort_loop[next_sort] |
||
405 | elif self.pressedkey == curses.KEY_RIGHT: |
||
406 | # ">" (right arrow) navigation through process sort |
||
407 | setattr(glances_processes, 'auto_sort', False) |
||
408 | next_sort = (self.loop_position() + 1) % len(self._sort_loop) |
||
409 | glances_processes.sort_key = self._sort_loop[next_sort] |
||
410 | |||
411 | # Return the key code |
||
412 | return self.pressedkey |
||
413 | |||
414 | def loop_position(self): |
||
415 | """Return the current sort in the loop""" |
||
416 | for i, v in enumerate(self._sort_loop): |
||
417 | if v == glances_processes.sort_key: |
||
418 | return i |
||
419 | return 0 |
||
420 | |||
421 | def disable_top(self): |
||
422 | """Disable the top panel""" |
||
423 | for p in ['quicklook', 'cpu', 'gpu', 'mem', 'memswap', 'load']: |
||
424 | setattr(self.args, 'disable_' + p, True) |
||
425 | |||
426 | def enable_top(self): |
||
427 | """Enable the top panel""" |
||
428 | for p in ['quicklook', 'cpu', 'gpu', 'mem', 'memswap', 'load']: |
||
429 | setattr(self.args, 'disable_' + p, False) |
||
430 | |||
431 | def disable_fullquicklook(self): |
||
432 | """Disable the full quicklook mode""" |
||
433 | for p in ['quicklook', 'cpu', 'gpu', 'mem', 'memswap']: |
||
434 | setattr(self.args, 'disable_' + p, False) |
||
435 | |||
436 | def enable_fullquicklook(self): |
||
437 | """Disable the full quicklook mode""" |
||
438 | self.args.disable_quicklook = False |
||
439 | for p in ['cpu', 'gpu', 'mem', 'memswap']: |
||
440 | setattr(self.args, 'disable_' + p, True) |
||
441 | |||
442 | def end(self): |
||
443 | """Shutdown the curses window.""" |
||
444 | if hasattr(curses, 'echo'): |
||
445 | curses.echo() |
||
446 | if hasattr(curses, 'nocbreak'): |
||
447 | curses.nocbreak() |
||
448 | if hasattr(curses, 'curs_set'): |
||
449 | try: |
||
450 | curses.curs_set(1) |
||
451 | except Exception: |
||
452 | pass |
||
453 | curses.endwin() |
||
454 | |||
455 | def init_line_column(self): |
||
456 | """Init the line and column position for the curses interface.""" |
||
457 | self.init_line() |
||
458 | self.init_column() |
||
459 | |||
460 | def init_line(self): |
||
461 | """Init the line position for the curses interface.""" |
||
462 | self.line = 0 |
||
463 | self.next_line = 0 |
||
464 | |||
465 | def init_column(self): |
||
466 | """Init the column position for the curses interface.""" |
||
467 | self.column = 0 |
||
468 | self.next_column = 0 |
||
469 | |||
470 | def new_line(self): |
||
471 | """New line in the curses interface.""" |
||
472 | self.line = self.next_line |
||
473 | |||
474 | def new_column(self): |
||
475 | """New column in the curses interface.""" |
||
476 | self.column = self.next_column |
||
477 | |||
478 | def __get_stat_display(self, stats, layer): |
||
479 | """Return a dict of dict with all the stats display. |
||
480 | stats: Global stats dict |
||
481 | layer: ~ cs_status |
||
482 | "None": standalone or server mode |
||
483 | "Connected": Client is connected to a Glances server |
||
484 | "SNMP": Client is connected to a SNMP server |
||
485 | "Disconnected": Client is disconnected from the server |
||
486 | |||
487 | :returns: dict of dict |
||
488 | * key: plugin name |
||
489 | * value: dict returned by the get_stats_display Plugin method |
||
490 | """ |
||
491 | ret = {} |
||
492 | |||
493 | for p in stats.getPluginsList(enable=False): |
||
494 | if p == 'quicklook' or p == 'processlist': |
||
495 | # processlist is done later |
||
496 | # because we need to know how many processes could be displayed |
||
497 | continue |
||
498 | |||
499 | # Compute the plugin max size |
||
500 | plugin_max_width = None |
||
501 | if p in self._left_sidebar: |
||
502 | plugin_max_width = max(self._left_sidebar_min_width, |
||
503 | self.screen.getmaxyx()[1] - 105) |
||
504 | plugin_max_width = min(self._left_sidebar_max_width, |
||
505 | plugin_max_width) |
||
506 | |||
507 | # Get the view |
||
508 | ret[p] = stats.get_plugin(p).get_stats_display(args=self.args, |
||
509 | max_width=plugin_max_width) |
||
510 | |||
511 | return ret |
||
512 | |||
513 | def display(self, stats, cs_status=None): |
||
514 | """Display stats on the screen. |
||
515 | |||
516 | stats: Stats database to display |
||
517 | cs_status: |
||
518 | "None": standalone or server mode |
||
519 | "Connected": Client is connected to a Glances server |
||
520 | "SNMP": Client is connected to a SNMP server |
||
521 | "Disconnected": Client is disconnected from the server |
||
522 | |||
523 | Return: |
||
524 | True if the stats have been displayed |
||
525 | False if the help have been displayed |
||
526 | """ |
||
527 | # Init the internal line/column for Glances Curses |
||
528 | self.init_line_column() |
||
529 | |||
530 | # Update the stats messages |
||
531 | ########################### |
||
532 | |||
533 | # Get all the plugins but quicklook and proceslist |
||
534 | self.args.cs_status = cs_status |
||
535 | __stat_display = self.__get_stat_display(stats, layer=cs_status) |
||
536 | |||
537 | # Adapt number of processes to the available space |
||
538 | max_processes_displayed = ( |
||
539 | self.screen.getmaxyx()[0] - 11 - |
||
540 | (0 if 'docker' not in __stat_display else |
||
541 | self.get_stats_display_height(__stat_display["docker"])) - |
||
542 | (0 if 'processcount' not in __stat_display else |
||
543 | self.get_stats_display_height(__stat_display["processcount"])) - |
||
544 | (0 if 'amps' not in __stat_display else |
||
545 | self.get_stats_display_height(__stat_display["amps"])) - |
||
546 | (0 if 'alert' not in __stat_display else |
||
547 | self.get_stats_display_height(__stat_display["alert"]))) |
||
548 | |||
549 | try: |
||
550 | if self.args.enable_process_extended: |
||
551 | max_processes_displayed -= 4 |
||
552 | except AttributeError: |
||
553 | pass |
||
554 | if max_processes_displayed < 0: |
||
555 | max_processes_displayed = 0 |
||
556 | if (glances_processes.max_processes is None or |
||
557 | glances_processes.max_processes != max_processes_displayed): |
||
558 | logger.debug("Set number of displayed processes to {}".format(max_processes_displayed)) |
||
559 | glances_processes.max_processes = max_processes_displayed |
||
560 | |||
561 | # Get the processlist |
||
562 | __stat_display["processlist"] = stats.get_plugin( |
||
563 | 'processlist').get_stats_display(args=self.args) |
||
564 | |||
565 | # Display the stats on the curses interface |
||
566 | ########################################### |
||
567 | |||
568 | # Help screen (on top of the other stats) |
||
569 | if self.args.help_tag: |
||
570 | # Display the stats... |
||
571 | self.display_plugin( |
||
572 | stats.get_plugin('help').get_stats_display(args=self.args)) |
||
573 | # ... and exit |
||
574 | return False |
||
575 | |||
576 | # ===================================== |
||
577 | # Display first line (system+ip+uptime) |
||
578 | # Optionnaly: Cloud on second line |
||
579 | # ===================================== |
||
580 | self.__display_header(__stat_display) |
||
581 | |||
582 | # ============================================================== |
||
583 | # Display second line (<SUMMARY>+CPU|PERCPU+<GPU>+LOAD+MEM+SWAP) |
||
584 | # ============================================================== |
||
585 | self.__display_top(__stat_display, stats) |
||
586 | |||
587 | # ================================================================== |
||
588 | # Display left sidebar (NETWORK+PORTS+DISKIO+FS+SENSORS+Current time) |
||
589 | # ================================================================== |
||
590 | self.__display_left(__stat_display) |
||
591 | |||
592 | # ==================================== |
||
593 | # Display right stats (process and co) |
||
594 | # ==================================== |
||
595 | self.__display_right(__stat_display) |
||
596 | |||
597 | # ===================== |
||
598 | # Others popup messages |
||
599 | # ===================== |
||
600 | |||
601 | # Display edit filter popup |
||
602 | # Only in standalone mode (cs_status is None) |
||
603 | if self.edit_filter and cs_status is None: |
||
604 | new_filter = self.display_popup( |
||
605 | 'Process filter pattern: \n\n' + |
||
606 | 'Examples:\n' + |
||
607 | '- python\n' + |
||
608 | '- .*python.*\n' + |
||
609 | '- /usr/lib.*\n' + |
||
610 | '- name:.*nautilus.*\n' + |
||
611 | '- cmdline:.*glances.*\n' + |
||
612 | '- username:nicolargo\n' + |
||
613 | '- username:^root ', |
||
614 | is_input=True, |
||
615 | input_value=glances_processes.process_filter_input) |
||
616 | glances_processes.process_filter = new_filter |
||
617 | elif self.edit_filter and cs_status is not None: |
||
618 | self.display_popup('Process filter only available in standalone mode') |
||
619 | self.edit_filter = False |
||
620 | |||
621 | # Display graph generation popup |
||
622 | if self.args.generate_graph: |
||
623 | self.display_popup('Generate graph in {}'.format(self.args.export_graph_path)) |
||
624 | |||
625 | return True |
||
626 | |||
627 | def __display_header(self, stat_display): |
||
628 | """Display the firsts lines (header) in the Curses interface. |
||
629 | |||
630 | system + ip + uptime |
||
631 | (cloud) |
||
632 | """ |
||
633 | # First line |
||
634 | self.new_line() |
||
635 | self.space_between_column = 0 |
||
636 | l_uptime = 1 |
||
637 | for i in ['system', 'ip', 'uptime']: |
||
638 | if i in stat_display: |
||
639 | l_uptime += self.get_stats_display_width(stat_display[i]) |
||
640 | self.display_plugin( |
||
641 | stat_display["system"], |
||
642 | display_optional=(self.screen.getmaxyx()[1] >= l_uptime)) |
||
643 | self.space_between_column = 3 |
||
644 | self.new_column() |
||
645 | self.display_plugin(stat_display["ip"]) |
||
646 | self.new_column() |
||
647 | self.display_plugin( |
||
648 | stat_display["uptime"], |
||
649 | add_space=-(self.get_stats_display_width(stat_display["cloud"]) != 0)) |
||
650 | # Second line (optional) |
||
651 | self.init_column() |
||
652 | self.new_line() |
||
653 | self.display_plugin(stat_display["cloud"]) |
||
654 | |||
655 | def __display_top(self, stat_display, stats): |
||
656 | """Display the second line in the Curses interface. |
||
657 | |||
658 | <QUICKLOOK> + CPU|PERCPU + <GPU> + MEM + SWAP + LOAD |
||
659 | """ |
||
660 | self.init_column() |
||
661 | self.new_line() |
||
662 | |||
663 | # Init quicklook |
||
664 | stat_display['quicklook'] = {'msgdict': []} |
||
665 | |||
666 | # Dict for plugins width |
||
667 | plugin_widths = {} |
||
668 | for p in self._top: |
||
669 | plugin_widths[p] = self.get_stats_display_width(stat_display.get(p, 0)) if hasattr(self.args, 'disable_' + p) else 0 |
||
670 | |||
671 | # Width of all plugins |
||
672 | stats_width = sum(itervalues(plugin_widths)) |
||
673 | |||
674 | # Number of plugin but quicklook |
||
675 | stats_number = sum([int(stat_display[p]['msgdict'] != []) for p in self._top if not getattr(self.args, 'disable_' + p)]) |
||
676 | |||
677 | if not self.args.disable_quicklook: |
||
678 | # Quick look is in the place ! |
||
679 | if self.args.full_quicklook: |
||
680 | quicklook_width = self.screen.getmaxyx()[1] - (stats_width + 8 + stats_number * self.space_between_column) |
||
681 | else: |
||
682 | quicklook_width = min(self.screen.getmaxyx()[1] - (stats_width + 8 + stats_number * self.space_between_column), |
||
683 | self._quicklook_max_width - 5) |
||
684 | try: |
||
685 | stat_display["quicklook"] = stats.get_plugin( |
||
686 | 'quicklook').get_stats_display(max_width=quicklook_width, args=self.args) |
||
687 | except AttributeError as e: |
||
688 | logger.debug("Quicklook plugin not available (%s)" % e) |
||
689 | else: |
||
690 | plugin_widths['quicklook'] = self.get_stats_display_width(stat_display["quicklook"]) |
||
691 | stats_width = sum(itervalues(plugin_widths)) + 1 |
||
692 | self.space_between_column = 1 |
||
693 | self.display_plugin(stat_display["quicklook"]) |
||
694 | self.new_column() |
||
695 | |||
696 | # Compute spaces between plugins |
||
697 | # Note: Only one space between Quicklook and others |
||
698 | plugin_display_optional = {} |
||
699 | for p in self._top: |
||
700 | plugin_display_optional[p] = True |
||
701 | if stats_number > 1: |
||
702 | self.space_between_column = max(1, int((self.screen.getmaxyx()[1] - stats_width) / (stats_number - 1))) |
||
703 | for p in ['mem', 'cpu']: |
||
704 | # No space ? Remove optional stats |
||
705 | if self.space_between_column < 3: |
||
706 | plugin_display_optional[p] = False |
||
707 | plugin_widths[p] = self.get_stats_display_width(stat_display[p], without_option=True) if hasattr(self.args, 'disable_' + p) else 0 |
||
708 | stats_width = sum(itervalues(plugin_widths)) + 1 |
||
709 | self.space_between_column = max(1, int((self.screen.getmaxyx()[1] - stats_width) / (stats_number - 1))) |
||
710 | else: |
||
711 | self.space_between_column = 0 |
||
712 | |||
713 | # Display CPU, MEM, SWAP and LOAD |
||
714 | for p in self._top: |
||
715 | if p == 'quicklook': |
||
716 | continue |
||
717 | if p in stat_display: |
||
718 | self.display_plugin(stat_display[p], |
||
719 | display_optional=plugin_display_optional[p]) |
||
720 | if p is not 'load': |
||
721 | # Skip last column |
||
722 | self.new_column() |
||
723 | |||
724 | # Space between column |
||
725 | self.space_between_column = 3 |
||
726 | |||
727 | # Backup line position |
||
728 | self.saved_line = self.next_line |
||
729 | |||
730 | def __display_left(self, stat_display): |
||
731 | """Display the left sidebar in the Curses interface.""" |
||
732 | self.init_column() |
||
733 | |||
734 | if self.args.disable_left_sidebar: |
||
735 | return |
||
736 | |||
737 | for p in self._left_sidebar: |
||
738 | if ((hasattr(self.args, 'enable_' + p) or |
||
739 | hasattr(self.args, 'disable_' + p)) and p in stat_display): |
||
740 | self.new_line() |
||
741 | self.display_plugin(stat_display[p]) |
||
742 | |||
743 | def __display_right(self, stat_display): |
||
744 | """Display the right sidebar in the Curses interface. |
||
745 | |||
746 | docker + processcount + amps + processlist + alert |
||
747 | """ |
||
748 | # Do not display anything if space is not available... |
||
749 | if self.screen.getmaxyx()[1] < self._left_sidebar_min_width: |
||
750 | return |
||
751 | |||
752 | # Restore line position |
||
753 | self.next_line = self.saved_line |
||
754 | |||
755 | # Display right sidebar |
||
756 | self.new_column() |
||
757 | for p in self._right_sidebar: |
||
758 | if ((hasattr(self.args, 'enable_' + p) or |
||
759 | hasattr(self.args, 'disable_' + p)) and p in stat_display): |
||
760 | if p not in p: |
||
761 | # Catch for issue #1470 |
||
762 | continue |
||
763 | self.new_line() |
||
764 | if p == 'processlist': |
||
765 | self.display_plugin(stat_display['processlist'], |
||
766 | display_optional=(self.screen.getmaxyx()[1] > 102), |
||
767 | display_additional=(not MACOS), |
||
768 | max_y=(self.screen.getmaxyx()[0] - self.get_stats_display_height(stat_display['alert']) - 2)) |
||
769 | else: |
||
770 | self.display_plugin(stat_display[p]) |
||
771 | |||
772 | def display_popup(self, message, |
||
773 | size_x=None, size_y=None, |
||
774 | duration=3, |
||
775 | is_input=False, |
||
776 | input_size=30, |
||
777 | input_value=None): |
||
778 | """ |
||
779 | Display a centered popup. |
||
780 | |||
781 | If is_input is False: |
||
782 | Display a centered popup with the given message during duration seconds |
||
783 | If size_x and size_y: set the popup size |
||
784 | else set it automatically |
||
785 | Return True if the popup could be displayed |
||
786 | |||
787 | If is_input is True: |
||
788 | Display a centered popup with the given message and a input field |
||
789 | If size_x and size_y: set the popup size |
||
790 | else set it automatically |
||
791 | Return the input string or None if the field is empty |
||
792 | """ |
||
793 | # Center the popup |
||
794 | sentence_list = message.split('\n') |
||
795 | if size_x is None: |
||
796 | size_x = len(max(sentence_list, key=len)) + 4 |
||
797 | # Add space for the input field |
||
798 | if is_input: |
||
799 | size_x += input_size |
||
800 | if size_y is None: |
||
801 | size_y = len(sentence_list) + 4 |
||
802 | screen_x = self.screen.getmaxyx()[1] |
||
803 | screen_y = self.screen.getmaxyx()[0] |
||
804 | if size_x > screen_x or size_y > screen_y: |
||
805 | # No size to display the popup => abord |
||
806 | return False |
||
807 | pos_x = int((screen_x - size_x) / 2) |
||
808 | pos_y = int((screen_y - size_y) / 2) |
||
809 | |||
810 | # Create the popup |
||
811 | popup = curses.newwin(size_y, size_x, pos_y, pos_x) |
||
812 | |||
813 | # Fill the popup |
||
814 | popup.border() |
||
815 | |||
816 | # Add the message |
||
817 | for y, m in enumerate(message.split('\n')): |
||
818 | popup.addnstr(2 + y, 2, m, len(m)) |
||
819 | |||
820 | if is_input and not WINDOWS: |
||
821 | # Create a subwindow for the text field |
||
822 | subpop = popup.derwin(1, input_size, 2, 2 + len(m)) |
||
823 | subpop.attron(self.colors_list['FILTER']) |
||
824 | # Init the field with the current value |
||
825 | if input_value is not None: |
||
826 | subpop.addnstr(0, 0, input_value, len(input_value)) |
||
827 | # Display the popup |
||
828 | popup.refresh() |
||
829 | subpop.refresh() |
||
830 | # Create the textbox inside the subwindows |
||
831 | self.set_cursor(2) |
||
832 | self.term_window.keypad(1) |
||
833 | textbox = GlancesTextbox(subpop, insert_mode=False) |
||
834 | textbox.edit() |
||
835 | self.set_cursor(0) |
||
836 | self.term_window.keypad(0) |
||
837 | if textbox.gather() != '': |
||
838 | logger.debug( |
||
839 | "User enters the following string: %s" % textbox.gather()) |
||
840 | return textbox.gather()[:-1] |
||
841 | else: |
||
842 | logger.debug("User centers an empty string") |
||
843 | return None |
||
844 | else: |
||
845 | # Display the popup |
||
846 | popup.refresh() |
||
847 | self.wait(duration * 1000) |
||
848 | return True |
||
849 | |||
850 | def display_plugin(self, plugin_stats, |
||
851 | display_optional=True, |
||
852 | display_additional=True, |
||
853 | max_y=65535, |
||
854 | add_space=0): |
||
855 | """Display the plugin_stats on the screen. |
||
856 | |||
857 | If display_optional=True display the optional stats |
||
858 | If display_additional=True display additionnal stats |
||
859 | max_y: do not display line > max_y |
||
860 | add_space: add x space (line) after the plugin |
||
861 | """ |
||
862 | # Exit if: |
||
863 | # - the plugin_stats message is empty |
||
864 | # - the display tag = False |
||
865 | if plugin_stats is None or not plugin_stats['msgdict'] or not plugin_stats['display']: |
||
866 | # Exit |
||
867 | return 0 |
||
868 | |||
869 | # Get the screen size |
||
870 | screen_x = self.screen.getmaxyx()[1] |
||
871 | screen_y = self.screen.getmaxyx()[0] |
||
872 | |||
873 | # Set the upper/left position of the message |
||
874 | if plugin_stats['align'] == 'right': |
||
875 | # Right align (last column) |
||
876 | display_x = screen_x - self.get_stats_display_width(plugin_stats) |
||
877 | else: |
||
878 | display_x = self.column |
||
879 | if plugin_stats['align'] == 'bottom': |
||
880 | # Bottom (last line) |
||
881 | display_y = screen_y - self.get_stats_display_height(plugin_stats) |
||
882 | else: |
||
883 | display_y = self.line |
||
884 | |||
885 | # Display |
||
886 | x = display_x |
||
887 | x_max = x |
||
888 | y = display_y |
||
889 | for m in plugin_stats['msgdict']: |
||
890 | # New line |
||
891 | if m['msg'].startswith('\n'): |
||
892 | # Go to the next line |
||
893 | y += 1 |
||
894 | # Return to the first column |
||
895 | x = display_x |
||
896 | continue |
||
897 | # Do not display outside the screen |
||
898 | if x < 0: |
||
899 | continue |
||
900 | if not m['splittable'] and (x + len(m['msg']) > screen_x): |
||
901 | continue |
||
902 | if y < 0 or (y + 1 > screen_y) or (y > max_y): |
||
903 | break |
||
904 | # If display_optional = False do not display optional stats |
||
905 | if not display_optional and m['optional']: |
||
906 | continue |
||
907 | # If display_additional = False do not display additional stats |
||
908 | if not display_additional and m['additional']: |
||
909 | continue |
||
910 | # Is it possible to display the stat with the current screen size |
||
911 | # !!! Crach if not try/except... Why ??? |
||
912 | try: |
||
913 | self.term_window.addnstr(y, x, |
||
914 | m['msg'], |
||
915 | # Do not disply outside the screen |
||
916 | screen_x - x, |
||
917 | self.colors_list[m['decoration']]) |
||
918 | except Exception: |
||
919 | pass |
||
920 | else: |
||
921 | # New column |
||
922 | # Python 2: we need to decode to get real screen size because |
||
923 | # UTF-8 special tree chars occupy several bytes. |
||
924 | # Python 3: strings are strings and bytes are bytes, all is |
||
925 | # good. |
||
926 | try: |
||
927 | x += len(u(m['msg'])) |
||
928 | except UnicodeDecodeError: |
||
929 | # Quick and dirty hack for issue #745 |
||
930 | pass |
||
931 | if x > x_max: |
||
932 | x_max = x |
||
933 | |||
934 | # Compute the next Glances column/line position |
||
935 | self.next_column = max( |
||
936 | self.next_column, x_max + self.space_between_column) |
||
937 | self.next_line = max(self.next_line, y + self.space_between_line) |
||
938 | |||
939 | # Have empty lines after the plugins |
||
940 | self.next_line += add_space |
||
941 | |||
942 | def erase(self): |
||
943 | """Erase the content of the screen.""" |
||
944 | self.term_window.erase() |
||
945 | |||
946 | def flush(self, stats, cs_status=None): |
||
947 | """Clear and update the screen. |
||
948 | |||
949 | stats: Stats database to display |
||
950 | cs_status: |
||
951 | "None": standalone or server mode |
||
952 | "Connected": Client is connected to the server |
||
953 | "Disconnected": Client is disconnected from the server |
||
954 | """ |
||
955 | self.erase() |
||
956 | self.display(stats, cs_status=cs_status) |
||
957 | |||
958 | def update(self, |
||
959 | stats, |
||
960 | duration=3, |
||
961 | cs_status=None, |
||
962 | return_to_browser=False): |
||
963 | """Update the screen. |
||
964 | |||
965 | INPUT |
||
966 | stats: Stats database to display |
||
967 | duration: duration of the loop |
||
968 | cs_status: |
||
969 | "None": standalone or server mode |
||
970 | "Connected": Client is connected to the server |
||
971 | "Disconnected": Client is disconnected from the server |
||
972 | return_to_browser: |
||
973 | True: Do not exist, return to the browser list |
||
974 | False: Exit and return to the shell |
||
975 | |||
976 | OUTPUT |
||
977 | True: Exit key has been pressed |
||
978 | False: Others cases... |
||
979 | """ |
||
980 | # Flush display |
||
981 | self.flush(stats, cs_status=cs_status) |
||
982 | |||
983 | # If the duration is < 0 (update + export time > refresh_time) |
||
984 | # Then display the interface and log a message |
||
985 | if duration <= 0: |
||
986 | logger.warning('Update and export time higher than refresh_time.') |
||
987 | duration = 0.1 |
||
988 | |||
989 | # Wait duration (in s) time |
||
990 | exitkey = False |
||
991 | countdown = Timer(duration) |
||
992 | # Set the default timeout (in ms) for the getch method |
||
993 | self.term_window.timeout(int(duration * 1000)) |
||
994 | while not countdown.finished() and not exitkey: |
||
995 | # Getkey |
||
996 | pressedkey = self.__catch_key(return_to_browser=return_to_browser) |
||
997 | # Is it an exit key ? |
||
998 | exitkey = (pressedkey == ord('\x1b') or pressedkey == ord('q')) |
||
999 | if not exitkey and pressedkey > -1: |
||
1000 | # Redraw display |
||
1001 | self.flush(stats, cs_status=cs_status) |
||
1002 | # Overwrite the timeout with the countdown |
||
1003 | self.term_window.timeout(int(countdown.get() * 1000)) |
||
1004 | |||
1005 | return exitkey |
||
1006 | |||
1007 | def wait(self, delay=100): |
||
1008 | """Wait delay in ms""" |
||
1009 | curses.napms(100) |
||
1010 | |||
1011 | def get_stats_display_width(self, curse_msg, without_option=False): |
||
1012 | """Return the width of the formatted curses message.""" |
||
1013 | try: |
||
1014 | if without_option: |
||
1015 | # Size without options |
||
1016 | c = len(max(''.join([(u(u(nativestr(i['msg'])).encode('ascii', 'replace')) if not i['optional'] else "") |
||
1017 | for i in curse_msg['msgdict']]).split('\n'), key=len)) |
||
1018 | else: |
||
1019 | # Size with all options |
||
1020 | c = len(max(''.join([u(u(nativestr(i['msg'])).encode('ascii', 'replace')) |
||
1021 | for i in curse_msg['msgdict']]).split('\n'), key=len)) |
||
1022 | except Exception as e: |
||
1023 | logger.debug('ERROR: Can not compute plugin width ({})'.format(e)) |
||
1024 | return 0 |
||
1025 | else: |
||
1026 | return c |
||
1027 | |||
1028 | def get_stats_display_height(self, curse_msg): |
||
1029 | r"""Return the height of the formatted curses message. |
||
1030 | |||
1031 | The height is defined by the number of '\n' (new line). |
||
1032 | """ |
||
1033 | try: |
||
1034 | c = [i['msg'] for i in curse_msg['msgdict']].count('\n') |
||
1035 | except Exception as e: |
||
1036 | logger.debug('ERROR: Can not compute plugin height ({})'.format(e)) |
||
1037 | return 0 |
||
1038 | else: |
||
1039 | return c + 1 |
||
1040 | |||
1041 | |||
1042 | class GlancesCursesStandalone(_GlancesCurses): |
||
1043 | |||
1044 | """Class for the Glances curse standalone.""" |
||
1045 | |||
1046 | pass |
||
1047 | |||
1048 | |||
1049 | class GlancesCursesClient(_GlancesCurses): |
||
1050 | |||
1051 | """Class for the Glances curse client.""" |
||
1052 | |||
1053 | pass |
||
1054 | |||
1055 | |||
1056 | if not WINDOWS: |
||
1057 | class GlancesTextbox(Textbox, object): |
||
1058 | |||
1059 | def __init__(self, *args, **kwargs): |
||
1060 | super(GlancesTextbox, self).__init__(*args, **kwargs) |
||
1061 | |||
1062 | def do_command(self, ch): |
||
1063 | if ch == 10: # Enter |
||
1064 | return 0 |
||
1065 | if ch == 127: # Back |
||
1066 | return 8 |
||
1067 | return super(GlancesTextbox, self).do_command(ch) |
||
1068 |
This check looks for calls to members that are non-existent. These calls will fail.
The member could have been renamed or removed.