1 | # -*- coding: utf-8 -*- |
||
2 | # |
||
3 | # This file is part of Glances. |
||
4 | # |
||
5 | # Copyright (C) 2016 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 | |||
22 | import re |
||
23 | import sys |
||
24 | |||
25 | from glances.compat import u |
||
26 | from glances.globals import OSX, WINDOWS |
||
27 | from glances.logger import logger |
||
28 | from glances.logs import glances_logs |
||
29 | from glances.processes import glances_processes |
||
30 | from glances.timer import Timer |
||
31 | |||
32 | # Import curses lib for "normal" operating system and consolelog for Windows |
||
33 | if not WINDOWS: |
||
34 | try: |
||
35 | import curses |
||
36 | import curses.panel |
||
37 | from curses.textpad import Textbox |
||
38 | except ImportError: |
||
39 | logger.critical( |
||
40 | "Curses module not found. Glances cannot start in standalone mode.") |
||
41 | sys.exit(1) |
||
42 | else: |
||
43 | from glances.outputs.glances_colorconsole import WCurseLight |
||
44 | curses = WCurseLight() |
||
45 | |||
46 | |||
47 | class _GlancesCurses(object): |
||
48 | |||
49 | """This class manages the curses display (and key pressed). |
||
50 | |||
51 | Note: It is a private class, use GlancesCursesClient or GlancesCursesBrowser. |
||
52 | """ |
||
53 | |||
54 | def __init__(self, args=None): |
||
55 | # Init args |
||
56 | self.args = args |
||
57 | |||
58 | # Init windows positions |
||
59 | self.term_w = 80 |
||
60 | self.term_h = 24 |
||
61 | |||
62 | # Space between stats |
||
63 | self.space_between_column = 3 |
||
64 | self.space_between_line = 2 |
||
65 | |||
66 | # Init the curses screen |
||
67 | self.screen = curses.initscr() |
||
68 | if not self.screen: |
||
69 | logger.critical("Cannot init the curses library.\n") |
||
70 | sys.exit(1) |
||
71 | |||
72 | # Init cursor |
||
73 | self._init_cursor() |
||
74 | |||
75 | # Init the colors |
||
76 | self._init_colors() |
||
77 | |||
78 | # Init main window |
||
79 | self.term_window = self.screen.subwin(0, 0) |
||
80 | |||
81 | # Init refresh time |
||
82 | self.__refresh_time = args.time |
||
83 | |||
84 | # Init edit filter tag |
||
85 | self.edit_filter = False |
||
86 | |||
87 | # Init the process min/max reset |
||
88 | self.args.reset_minmax_tag = False |
||
89 | |||
90 | # Catch key pressed with non blocking mode |
||
91 | self.no_flash_cursor() |
||
92 | self.term_window.nodelay(1) |
||
93 | self.pressedkey = -1 |
||
94 | |||
95 | # History tag |
||
96 | self._init_history() |
||
97 | |||
98 | def _init_history(self): |
||
99 | '''Init the history option''' |
||
100 | |||
101 | self.reset_history_tag = False |
||
102 | self.history_tag = False |
||
103 | if self.args.enable_history: |
||
104 | logger.info('Stats history enabled with output path %s' % |
||
105 | self.args.path_history) |
||
106 | from glances.exports.glances_history import GlancesHistory |
||
107 | self.glances_history = GlancesHistory(self.args.path_history) |
||
108 | if not self.glances_history.graph_enabled(): |
||
109 | self.args.enable_history = False |
||
110 | logger.error( |
||
111 | 'Stats history disabled because MatPlotLib is not installed') |
||
112 | |||
113 | def _init_cursor(self): |
||
114 | '''Init cursors''' |
||
115 | |||
116 | if hasattr(curses, 'noecho'): |
||
117 | curses.noecho() |
||
118 | if hasattr(curses, 'cbreak'): |
||
119 | curses.cbreak() |
||
120 | self.set_cursor(0) |
||
121 | |||
122 | def _init_colors(self): |
||
123 | '''Init the Curses color layout''' |
||
124 | |||
125 | # Set curses options |
||
126 | if hasattr(curses, 'start_color'): |
||
127 | curses.start_color() |
||
128 | if hasattr(curses, 'use_default_colors'): |
||
129 | curses.use_default_colors() |
||
130 | |||
131 | # Init colors |
||
132 | if self.args.disable_bold: |
||
133 | A_BOLD = 0 |
||
134 | self.args.disable_bg = True |
||
135 | else: |
||
136 | A_BOLD = curses.A_BOLD |
||
137 | |||
138 | self.title_color = A_BOLD |
||
139 | self.title_underline_color = A_BOLD | curses.A_UNDERLINE |
||
140 | self.help_color = A_BOLD |
||
141 | |||
142 | if curses.has_colors(): |
||
143 | # The screen is compatible with a colored design |
||
144 | if self.args.theme_white: |
||
145 | # White theme: black ==> white |
||
146 | curses.init_pair(1, curses.COLOR_BLACK, -1) |
||
147 | else: |
||
148 | curses.init_pair(1, curses.COLOR_WHITE, -1) |
||
149 | if self.args.disable_bg: |
||
150 | curses.init_pair(2, curses.COLOR_RED, -1) |
||
151 | curses.init_pair(3, curses.COLOR_GREEN, -1) |
||
152 | curses.init_pair(4, curses.COLOR_BLUE, -1) |
||
153 | curses.init_pair(5, curses.COLOR_MAGENTA, -1) |
||
154 | else: |
||
155 | curses.init_pair(2, curses.COLOR_WHITE, curses.COLOR_RED) |
||
156 | curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_GREEN) |
||
157 | curses.init_pair(4, curses.COLOR_WHITE, curses.COLOR_BLUE) |
||
158 | curses.init_pair(5, curses.COLOR_WHITE, curses.COLOR_MAGENTA) |
||
159 | curses.init_pair(6, curses.COLOR_RED, -1) |
||
160 | curses.init_pair(7, curses.COLOR_GREEN, -1) |
||
161 | curses.init_pair(8, curses.COLOR_BLUE, -1) |
||
162 | |||
163 | # Colors text styles |
||
164 | if curses.COLOR_PAIRS > 8: |
||
165 | try: |
||
166 | curses.init_pair(9, curses.COLOR_MAGENTA, -1) |
||
167 | except Exception: |
||
168 | if self.args.theme_white: |
||
169 | curses.init_pair(9, curses.COLOR_BLACK, -1) |
||
170 | else: |
||
171 | curses.init_pair(9, curses.COLOR_WHITE, -1) |
||
172 | try: |
||
173 | curses.init_pair(10, curses.COLOR_CYAN, -1) |
||
174 | except Exception: |
||
175 | if self.args.theme_white: |
||
176 | curses.init_pair(10, curses.COLOR_BLACK, -1) |
||
177 | else: |
||
178 | curses.init_pair(10, curses.COLOR_WHITE, -1) |
||
179 | |||
180 | self.ifWARNING_color2 = curses.color_pair(9) | A_BOLD |
||
181 | self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD |
||
182 | self.filter_color = curses.color_pair(10) | A_BOLD |
||
183 | |||
184 | self.no_color = curses.color_pair(1) |
||
185 | self.default_color = curses.color_pair(3) | A_BOLD |
||
186 | self.nice_color = curses.color_pair(9) | A_BOLD |
||
187 | self.cpu_time_color = curses.color_pair(9) | A_BOLD |
||
188 | self.ifCAREFUL_color = curses.color_pair(4) | A_BOLD |
||
189 | self.ifWARNING_color = curses.color_pair(5) | A_BOLD |
||
190 | self.ifCRITICAL_color = curses.color_pair(2) | A_BOLD |
||
191 | self.default_color2 = curses.color_pair(7) | A_BOLD |
||
192 | self.ifCAREFUL_color2 = curses.color_pair(8) | A_BOLD |
||
193 | |||
194 | else: |
||
195 | # The screen is NOT compatible with a colored design |
||
196 | # switch to B&W text styles |
||
197 | self.no_color = curses.A_NORMAL |
||
198 | self.default_color = curses.A_NORMAL |
||
199 | self.nice_color = A_BOLD |
||
200 | self.cpu_time_color = A_BOLD |
||
201 | self.ifCAREFUL_color = curses.A_UNDERLINE |
||
202 | self.ifWARNING_color = A_BOLD |
||
203 | self.ifCRITICAL_color = curses.A_REVERSE |
||
204 | self.default_color2 = curses.A_NORMAL |
||
205 | self.ifCAREFUL_color2 = curses.A_UNDERLINE |
||
206 | self.ifWARNING_color2 = A_BOLD |
||
207 | self.ifCRITICAL_color2 = curses.A_REVERSE |
||
208 | self.filter_color = A_BOLD |
||
209 | |||
210 | # Define the colors list (hash table) for stats |
||
211 | self.colors_list = { |
||
212 | 'DEFAULT': self.no_color, |
||
213 | 'UNDERLINE': curses.A_UNDERLINE, |
||
214 | 'BOLD': A_BOLD, |
||
215 | 'SORT': A_BOLD, |
||
216 | 'OK': self.default_color2, |
||
217 | 'FILTER': self.filter_color, |
||
218 | 'TITLE': self.title_color, |
||
219 | 'PROCESS': self.default_color2, |
||
220 | 'STATUS': self.default_color2, |
||
221 | 'NICE': self.nice_color, |
||
222 | 'CPU_TIME': self.cpu_time_color, |
||
223 | 'CAREFUL': self.ifCAREFUL_color2, |
||
224 | 'WARNING': self.ifWARNING_color2, |
||
225 | 'CRITICAL': self.ifCRITICAL_color2, |
||
226 | 'OK_LOG': self.default_color, |
||
227 | 'CAREFUL_LOG': self.ifCAREFUL_color, |
||
228 | 'WARNING_LOG': self.ifWARNING_color, |
||
229 | 'CRITICAL_LOG': self.ifCRITICAL_color, |
||
230 | 'PASSWORD': curses.A_PROTECT |
||
231 | } |
||
232 | |||
233 | def flash_cursor(self): |
||
234 | self.term_window.keypad(1) |
||
235 | |||
236 | def no_flash_cursor(self): |
||
237 | self.term_window.keypad(0) |
||
238 | |||
239 | def set_cursor(self, value): |
||
240 | """Configure the curse cursor apparence. |
||
241 | |||
242 | 0: invisible |
||
243 | 1: visible |
||
244 | 2: very visible |
||
245 | """ |
||
246 | if hasattr(curses, 'curs_set'): |
||
247 | try: |
||
248 | curses.curs_set(value) |
||
249 | except Exception: |
||
250 | pass |
||
251 | |||
252 | def get_key(self, window): |
||
253 | # Catch ESC key AND numlock key (issue #163) |
||
254 | keycode = [0, 0] |
||
255 | keycode[0] = window.getch() |
||
256 | keycode[1] = window.getch() |
||
257 | |||
258 | if keycode != [-1, -1]: |
||
259 | logger.debug("Keypressed (code: %s)" % keycode) |
||
260 | |||
261 | if keycode[0] == 27 and keycode[1] != -1: |
||
262 | # Do not escape on specials keys |
||
263 | return -1 |
||
264 | else: |
||
265 | return keycode[0] |
||
266 | |||
267 | def __catch_key(self, return_to_browser=False): |
||
268 | # Catch the pressed key |
||
269 | self.pressedkey = self.get_key(self.term_window) |
||
270 | |||
271 | # Actions... |
||
272 | if self.pressedkey == ord('\x1b') or self.pressedkey == ord('q'): |
||
273 | # 'ESC'|'q' > Quit |
||
274 | if return_to_browser: |
||
275 | logger.info("Stop Glances client and return to the browser") |
||
276 | else: |
||
277 | self.end() |
||
278 | logger.info("Stop Glances") |
||
279 | sys.exit(0) |
||
280 | elif self.pressedkey == 10: |
||
281 | # 'ENTER' > Edit the process filter |
||
282 | self.edit_filter = not self.edit_filter |
||
283 | elif self.pressedkey == ord('0'): |
||
284 | # '0' > Switch between IRIX and Solaris mode |
||
285 | self.args.disable_irix = not self.args.disable_irix |
||
286 | elif self.pressedkey == ord('1'): |
||
287 | # '1' > Switch between CPU and PerCPU information |
||
288 | self.args.percpu = not self.args.percpu |
||
289 | elif self.pressedkey == ord('2'): |
||
290 | # '2' > Enable/disable left sidebar |
||
291 | self.args.disable_left_sidebar = not self.args.disable_left_sidebar |
||
292 | elif self.pressedkey == ord('3'): |
||
293 | # '3' > Enable/disable quicklook |
||
294 | self.args.disable_quicklook = not self.args.disable_quicklook |
||
295 | elif self.pressedkey == ord('4'): |
||
296 | # '4' > Enable/disable all but quick look and load |
||
297 | self.args.full_quicklook = not self.args.full_quicklook |
||
298 | if self.args.full_quicklook: |
||
299 | self.args.disable_quicklook = False |
||
300 | self.args.disable_cpu = True |
||
301 | self.args.disable_mem = True |
||
302 | self.args.disable_swap = True |
||
303 | else: |
||
304 | self.args.disable_quicklook = False |
||
305 | self.args.disable_cpu = False |
||
306 | self.args.disable_mem = False |
||
307 | self.args.disable_swap = False |
||
308 | elif self.pressedkey == ord('5'): |
||
309 | # '5' > Enable/disable top menu |
||
310 | logger.info(self.args.disable_top) |
||
311 | self.args.disable_top = not self.args.disable_top |
||
312 | if self.args.disable_top: |
||
313 | self.args.disable_quicklook = True |
||
314 | self.args.disable_cpu = True |
||
315 | self.args.disable_mem = True |
||
316 | self.args.disable_swap = True |
||
317 | self.args.disable_load = True |
||
318 | else: |
||
319 | self.args.disable_quicklook = False |
||
320 | self.args.disable_cpu = False |
||
321 | self.args.disable_mem = False |
||
322 | self.args.disable_swap = False |
||
323 | self.args.disable_load = False |
||
324 | elif self.pressedkey == ord('/'): |
||
325 | # '/' > Switch between short/long name for processes |
||
326 | self.args.process_short_name = not self.args.process_short_name |
||
327 | elif self.pressedkey == ord('a'): |
||
328 | # 'a' > Sort processes automatically and reset to 'cpu_percent' |
||
329 | glances_processes.auto_sort = True |
||
330 | glances_processes.sort_key = 'cpu_percent' |
||
331 | elif self.pressedkey == ord('b'): |
||
332 | # 'b' > Switch between bit/s and Byte/s for network IO |
||
333 | self.args.byte = not self.args.byte |
||
334 | elif self.pressedkey == ord('B'): |
||
335 | # 'B' > Switch between bit/s and IO/s for Disk IO |
||
336 | self.args.diskio_iops = not self.args.diskio_iops |
||
337 | elif self.pressedkey == ord('c'): |
||
338 | # 'c' > Sort processes by CPU usage |
||
339 | glances_processes.auto_sort = False |
||
340 | glances_processes.sort_key = 'cpu_percent' |
||
341 | elif self.pressedkey == ord('d'): |
||
342 | # 'd' > Show/hide disk I/O stats |
||
343 | self.args.disable_diskio = not self.args.disable_diskio |
||
344 | elif self.pressedkey == ord('D'): |
||
345 | # 'D' > Show/hide Docker stats |
||
346 | self.args.disable_docker = not self.args.disable_docker |
||
347 | elif self.pressedkey == ord('e'): |
||
348 | # 'e' > Enable/Disable extended stats for top process |
||
349 | self.args.enable_process_extended = not self.args.enable_process_extended |
||
350 | if not self.args.enable_process_extended: |
||
351 | glances_processes.disable_extended() |
||
352 | else: |
||
353 | glances_processes.enable_extended() |
||
354 | elif self.pressedkey == ord('E'): |
||
355 | # 'E' > Erase the process filter |
||
356 | logger.info("Erase process filter") |
||
357 | glances_processes.process_filter = None |
||
358 | elif self.pressedkey == ord('F'): |
||
359 | # 'F' > Switch between FS available and free space |
||
360 | self.args.fs_free_space = not self.args.fs_free_space |
||
361 | elif self.pressedkey == ord('f'): |
||
362 | # 'f' > Show/hide fs / folder stats |
||
363 | self.args.disable_fs = not self.args.disable_fs |
||
364 | self.args.disable_folder = not self.args.disable_folder |
||
365 | elif self.pressedkey == ord('g'): |
||
366 | # 'g' > History |
||
367 | self.history_tag = not self.history_tag |
||
368 | elif self.pressedkey == ord('h'): |
||
369 | # 'h' > Show/hide help |
||
370 | self.args.help_tag = not self.args.help_tag |
||
371 | elif self.pressedkey == ord('i'): |
||
372 | # 'i' > Sort processes by IO rate (not available on OS X) |
||
373 | glances_processes.auto_sort = False |
||
374 | glances_processes.sort_key = 'io_counters' |
||
375 | elif self.pressedkey == ord('I'): |
||
376 | # 'I' > Show/hide IP module |
||
377 | self.args.disable_ip = not self.args.disable_ip |
||
378 | elif self.pressedkey == ord('l'): |
||
379 | # 'l' > Show/hide log messages |
||
380 | self.args.disable_log = not self.args.disable_log |
||
381 | elif self.pressedkey == ord('m'): |
||
382 | # 'm' > Sort processes by MEM usage |
||
383 | glances_processes.auto_sort = False |
||
384 | glances_processes.sort_key = 'memory_percent' |
||
385 | elif self.pressedkey == ord('M'): |
||
386 | # 'M' > Reset processes summary min/max |
||
387 | self.args.reset_minmax_tag = not self.args.reset_minmax_tag |
||
388 | elif self.pressedkey == ord('n'): |
||
389 | # 'n' > Show/hide network stats |
||
390 | self.args.disable_network = not self.args.disable_network |
||
391 | elif self.pressedkey == ord('p'): |
||
392 | # 'p' > Sort processes by name |
||
393 | glances_processes.auto_sort = False |
||
394 | glances_processes.sort_key = 'name' |
||
395 | elif self.pressedkey == ord('r'): |
||
396 | # 'r' > Reset history |
||
397 | self.reset_history_tag = not self.reset_history_tag |
||
398 | elif self.pressedkey == ord('R'): |
||
399 | # 'R' > Hide RAID plugins |
||
400 | self.args.disable_raid = not self.args.disable_raid |
||
401 | elif self.pressedkey == ord('s'): |
||
402 | # 's' > Show/hide sensors stats (Linux-only) |
||
403 | self.args.disable_sensors = not self.args.disable_sensors |
||
404 | elif self.pressedkey == ord('t'): |
||
405 | # 't' > Sort processes by TIME usage |
||
406 | glances_processes.auto_sort = False |
||
407 | glances_processes.sort_key = 'cpu_times' |
||
408 | elif self.pressedkey == ord('T'): |
||
409 | # 'T' > View network traffic as sum Rx+Tx |
||
410 | self.args.network_sum = not self.args.network_sum |
||
411 | elif self.pressedkey == ord('u'): |
||
412 | # 'u' > Sort processes by USER |
||
413 | glances_processes.auto_sort = False |
||
414 | glances_processes.sort_key = 'username' |
||
415 | elif self.pressedkey == ord('U'): |
||
416 | # 'U' > View cumulative network I/O (instead of bitrate) |
||
417 | self.args.network_cumul = not self.args.network_cumul |
||
418 | elif self.pressedkey == ord('w'): |
||
419 | # 'w' > Delete finished warning logs |
||
420 | glances_logs.clean() |
||
421 | elif self.pressedkey == ord('x'): |
||
422 | # 'x' > Delete finished warning and critical logs |
||
423 | glances_logs.clean(critical=True) |
||
424 | elif self.pressedkey == ord('z'): |
||
425 | # 'z' > Enable/Disable processes stats (count + list + monitor) |
||
426 | # Enable/Disable display |
||
427 | self.args.disable_process = not self.args.disable_process |
||
428 | # Enable/Disable update |
||
429 | if self.args.disable_process: |
||
430 | glances_processes.disable() |
||
431 | else: |
||
432 | glances_processes.enable() |
||
433 | # Return the key code |
||
434 | return self.pressedkey |
||
435 | |||
436 | def end(self): |
||
437 | """Shutdown the curses window.""" |
||
438 | if hasattr(curses, 'echo'): |
||
439 | curses.echo() |
||
440 | if hasattr(curses, 'nocbreak'): |
||
441 | curses.nocbreak() |
||
442 | if hasattr(curses, 'curs_set'): |
||
443 | try: |
||
444 | curses.curs_set(1) |
||
445 | except Exception: |
||
446 | pass |
||
447 | curses.endwin() |
||
448 | |||
449 | def init_line_column(self): |
||
450 | """Init the line and column position for the curses inteface.""" |
||
451 | self.init_line() |
||
452 | self.init_column() |
||
453 | |||
454 | def init_line(self): |
||
455 | """Init the line position for the curses inteface.""" |
||
456 | self.line = 0 |
||
457 | self.next_line = 0 |
||
458 | |||
459 | def init_column(self): |
||
460 | """Init the column position for the curses inteface.""" |
||
461 | self.column = 0 |
||
462 | self.next_column = 0 |
||
463 | |||
464 | def new_line(self): |
||
465 | """New line in the curses interface.""" |
||
466 | self.line = self.next_line |
||
467 | |||
468 | def new_column(self): |
||
469 | """New column in the curses interface.""" |
||
470 | self.column = self.next_column |
||
471 | |||
472 | def display(self, stats, cs_status=None): |
||
473 | """Display stats on the screen. |
||
474 | |||
475 | stats: Stats database to display |
||
476 | cs_status: |
||
477 | "None": standalone or server mode |
||
478 | "Connected": Client is connected to a Glances server |
||
479 | "SNMP": Client is connected to a SNMP server |
||
480 | "Disconnected": Client is disconnected from the server |
||
481 | |||
482 | Return: |
||
483 | True if the stats have been displayed |
||
484 | False if the help have been displayed |
||
485 | """ |
||
486 | # Init the internal line/column for Glances Curses |
||
487 | self.init_line_column() |
||
488 | |||
489 | # Get the screen size |
||
490 | screen_x = self.screen.getmaxyx()[1] |
||
491 | screen_y = self.screen.getmaxyx()[0] |
||
492 | |||
493 | # No processes list in SNMP mode |
||
494 | if cs_status == 'SNMP': |
||
495 | # so... more space for others plugins |
||
496 | plugin_max_width = 43 |
||
497 | else: |
||
498 | plugin_max_width = None |
||
499 | |||
500 | # Update the stats messages |
||
501 | ########################### |
||
502 | |||
503 | # Update the client server status |
||
504 | self.args.cs_status = cs_status |
||
505 | stats_system = stats.get_plugin( |
||
506 | 'system').get_stats_display(args=self.args) |
||
507 | stats_uptime = stats.get_plugin('uptime').get_stats_display() |
||
508 | if self.args.percpu: |
||
509 | stats_cpu = stats.get_plugin('percpu').get_stats_display(args=self.args) |
||
510 | else: |
||
511 | stats_cpu = stats.get_plugin('cpu').get_stats_display(args=self.args) |
||
512 | stats_load = stats.get_plugin('load').get_stats_display(args=self.args) |
||
513 | stats_mem = stats.get_plugin('mem').get_stats_display(args=self.args) |
||
514 | stats_memswap = stats.get_plugin('memswap').get_stats_display(args=self.args) |
||
515 | stats_network = stats.get_plugin('network').get_stats_display( |
||
516 | args=self.args, max_width=plugin_max_width) |
||
517 | try: |
||
518 | stats_ip = stats.get_plugin('ip').get_stats_display(args=self.args) |
||
519 | except AttributeError: |
||
520 | stats_ip = None |
||
521 | stats_diskio = stats.get_plugin( |
||
522 | 'diskio').get_stats_display(args=self.args) |
||
523 | stats_fs = stats.get_plugin('fs').get_stats_display( |
||
524 | args=self.args, max_width=plugin_max_width) |
||
525 | stats_folders = stats.get_plugin('folders').get_stats_display( |
||
526 | args=self.args, max_width=plugin_max_width) |
||
527 | stats_raid = stats.get_plugin('raid').get_stats_display( |
||
528 | args=self.args) |
||
529 | stats_sensors = stats.get_plugin( |
||
530 | 'sensors').get_stats_display(args=self.args) |
||
531 | stats_now = stats.get_plugin('now').get_stats_display() |
||
532 | stats_docker = stats.get_plugin('docker').get_stats_display( |
||
533 | args=self.args) |
||
534 | stats_processcount = stats.get_plugin( |
||
535 | 'processcount').get_stats_display(args=self.args) |
||
536 | stats_monitor = stats.get_plugin( |
||
537 | 'monitor').get_stats_display(args=self.args) |
||
538 | stats_alert = stats.get_plugin( |
||
539 | 'alert').get_stats_display(args=self.args) |
||
540 | |||
541 | # Adapt number of processes to the available space |
||
542 | max_processes_displayed = screen_y - 11 - \ |
||
543 | self.get_stats_display_height(stats_alert) - \ |
||
544 | self.get_stats_display_height(stats_docker) |
||
545 | try: |
||
546 | if self.args.enable_process_extended and not self.args.process_tree: |
||
547 | max_processes_displayed -= 4 |
||
548 | except AttributeError: |
||
549 | pass |
||
550 | if max_processes_displayed < 0: |
||
551 | max_processes_displayed = 0 |
||
552 | if (glances_processes.max_processes is None or |
||
553 | glances_processes.max_processes != max_processes_displayed): |
||
554 | logger.debug("Set number of displayed processes to {0}".format(max_processes_displayed)) |
||
555 | glances_processes.max_processes = max_processes_displayed |
||
556 | |||
557 | stats_processlist = stats.get_plugin( |
||
558 | 'processlist').get_stats_display(args=self.args) |
||
559 | |||
560 | # Display the stats on the curses interface |
||
561 | ########################################### |
||
562 | |||
563 | # Help screen (on top of the other stats) |
||
564 | if self.args.help_tag: |
||
565 | # Display the stats... |
||
566 | self.display_plugin( |
||
567 | stats.get_plugin('help').get_stats_display(args=self.args)) |
||
568 | # ... and exit |
||
569 | return False |
||
570 | |||
571 | # ================================== |
||
572 | # Display first line (system+uptime) |
||
573 | # ================================== |
||
574 | # Space between column |
||
575 | self.space_between_column = 0 |
||
576 | self.new_line() |
||
577 | l_uptime = self.get_stats_display_width( |
||
578 | stats_system) + self.space_between_column + self.get_stats_display_width(stats_ip) + 3 + self.get_stats_display_width(stats_uptime) |
||
579 | self.display_plugin( |
||
580 | stats_system, display_optional=(screen_x >= l_uptime)) |
||
581 | self.new_column() |
||
582 | self.display_plugin(stats_ip) |
||
583 | # Space between column |
||
584 | self.space_between_column = 3 |
||
585 | self.new_column() |
||
586 | self.display_plugin(stats_uptime) |
||
587 | |||
588 | # ======================================================== |
||
589 | # Display second line (<SUMMARY>+CPU|PERCPU+LOAD+MEM+SWAP) |
||
590 | # ======================================================== |
||
591 | self.init_column() |
||
592 | self.new_line() |
||
593 | |||
594 | # Init quicklook |
||
595 | stats_quicklook = {'msgdict': []} |
||
596 | quicklook_width = 0 |
||
597 | |||
598 | # Get stats for CPU, MEM, SWAP and LOAD (if needed) |
||
599 | if self.args.disable_cpu: |
||
600 | cpu_width = 0 |
||
601 | else: |
||
602 | cpu_width = self.get_stats_display_width(stats_cpu) |
||
603 | if self.args.disable_mem: |
||
604 | mem_width = 0 |
||
605 | else: |
||
606 | mem_width = self.get_stats_display_width(stats_mem) |
||
607 | if self.args.disable_swap: |
||
608 | swap_width = 0 |
||
609 | else: |
||
610 | swap_width = self.get_stats_display_width(stats_memswap) |
||
611 | if self.args.disable_load: |
||
612 | load_width = 0 |
||
613 | else: |
||
614 | load_width = self.get_stats_display_width(stats_load) |
||
615 | |||
616 | # Size of plugins but quicklook |
||
617 | stats_width = cpu_width + mem_width + swap_width + load_width |
||
618 | |||
619 | # Number of plugin but quicklook |
||
620 | stats_number = ( |
||
621 | int(not self.args.disable_cpu and stats_cpu['msgdict'] != []) + |
||
622 | int(not self.args.disable_mem and stats_mem['msgdict'] != []) + |
||
623 | int(not self.args.disable_swap and stats_memswap['msgdict'] != []) + |
||
624 | int(not self.args.disable_load and stats_load['msgdict'] != [])) |
||
625 | |||
626 | if not self.args.disable_quicklook: |
||
627 | # Quick look is in the place ! |
||
628 | if self.args.full_quicklook: |
||
629 | quicklook_width = screen_x - (stats_width + 8 + stats_number * self.space_between_column) |
||
630 | else: |
||
631 | quicklook_width = min(screen_x - (stats_width + 8 + stats_number * self.space_between_column), 79) |
||
632 | try: |
||
633 | stats_quicklook = stats.get_plugin( |
||
634 | 'quicklook').get_stats_display(max_width=quicklook_width, args=self.args) |
||
635 | except AttributeError as e: |
||
636 | logger.debug("Quicklook plugin not available (%s)" % e) |
||
637 | else: |
||
638 | quicklook_width = self.get_stats_display_width(stats_quicklook) |
||
639 | stats_width += quicklook_width + 1 |
||
640 | self.space_between_column = 1 |
||
641 | self.display_plugin(stats_quicklook) |
||
642 | self.new_column() |
||
643 | |||
644 | # Compute spaces between plugins |
||
645 | # Note: Only one space between Quicklook and others |
||
646 | display_optional_cpu = True |
||
647 | display_optional_mem = True |
||
648 | if stats_number > 1: |
||
649 | self.space_between_column = max(1, int((screen_x - stats_width) / (stats_number - 1))) |
||
650 | # No space ? Remove optionnal MEM stats |
||
651 | if self.space_between_column < 3: |
||
0 ignored issues
–
show
|
|||
652 | display_optional_mem = False |
||
653 | if self.args.disable_mem: |
||
654 | mem_width = 0 |
||
655 | else: |
||
656 | mem_width = self.get_stats_display_width(stats_mem, without_option=True) |
||
657 | stats_width = quicklook_width + 1 + cpu_width + mem_width + swap_width + load_width |
||
658 | self.space_between_column = max(1, int((screen_x - stats_width) / (stats_number - 1))) |
||
659 | # No space again ? Remove optionnal CPU stats |
||
660 | if self.space_between_column < 3: |
||
0 ignored issues
–
show
This code seems to be duplicated in your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository.
Loading history...
|
|||
661 | display_optional_cpu = False |
||
662 | if self.args.disable_cpu: |
||
663 | cpu_width = 0 |
||
664 | else: |
||
665 | cpu_width = self.get_stats_display_width(stats_cpu, without_option=True) |
||
666 | stats_width = quicklook_width + 1 + cpu_width + mem_width + swap_width + load_width |
||
667 | self.space_between_column = max(1, int((screen_x - stats_width) / (stats_number - 1))) |
||
668 | else: |
||
669 | self.space_between_column = 0 |
||
670 | |||
671 | # Display CPU, MEM, SWAP and LOAD |
||
672 | self.display_plugin(stats_cpu, display_optional=display_optional_cpu) |
||
673 | self.new_column() |
||
674 | self.display_plugin(stats_mem, display_optional=display_optional_mem) |
||
675 | self.new_column() |
||
676 | self.display_plugin(stats_memswap) |
||
677 | self.new_column() |
||
678 | self.display_plugin(stats_load) |
||
679 | |||
680 | # Space between column |
||
681 | self.space_between_column = 3 |
||
682 | |||
683 | # Backup line position |
||
684 | self.saved_line = self.next_line |
||
685 | |||
686 | # ================================================================== |
||
687 | # Display left sidebar (NETWORK+DISKIO+FS+SENSORS+Current time) |
||
688 | # ================================================================== |
||
689 | self.init_column() |
||
690 | if not (self.args.disable_network and |
||
691 | self.args.disable_diskio and |
||
692 | self.args.disable_fs and |
||
693 | self.args.disable_folder and |
||
694 | self.args.disable_raid and |
||
695 | self.args.disable_sensors) and not self.args.disable_left_sidebar: |
||
696 | self.new_line() |
||
697 | self.display_plugin(stats_network) |
||
698 | self.new_line() |
||
699 | self.display_plugin(stats_diskio) |
||
700 | self.new_line() |
||
701 | self.display_plugin(stats_fs) |
||
702 | self.new_line() |
||
703 | self.display_plugin(stats_folders) |
||
704 | self.new_line() |
||
705 | self.display_plugin(stats_raid) |
||
706 | self.new_line() |
||
707 | self.display_plugin(stats_sensors) |
||
708 | self.new_line() |
||
709 | self.display_plugin(stats_now) |
||
710 | |||
711 | # ==================================== |
||
712 | # Display right stats (process and co) |
||
713 | # ==================================== |
||
714 | # If space available... |
||
715 | if screen_x > 52: |
||
716 | # Restore line position |
||
717 | self.next_line = self.saved_line |
||
718 | |||
719 | # Display right sidebar |
||
720 | # ((DOCKER)+PROCESS_COUNT+(MONITORED)+PROCESS_LIST+ALERT) |
||
721 | self.new_column() |
||
722 | self.new_line() |
||
723 | self.display_plugin(stats_docker) |
||
724 | self.new_line() |
||
725 | self.display_plugin(stats_processcount) |
||
726 | if glances_processes.process_filter is None and cs_status is None: |
||
727 | # Do not display stats monitor list if a filter exist |
||
728 | self.new_line() |
||
729 | self.display_plugin(stats_monitor) |
||
730 | self.new_line() |
||
731 | self.display_plugin(stats_processlist, |
||
732 | display_optional=(screen_x > 102), |
||
733 | display_additional=(not OSX), |
||
734 | max_y=(screen_y - self.get_stats_display_height(stats_alert) - 2)) |
||
735 | self.new_line() |
||
736 | self.display_plugin(stats_alert) |
||
737 | |||
738 | # History option |
||
739 | # Generate history graph |
||
740 | if self.history_tag and self.args.enable_history: |
||
741 | self.display_popup( |
||
742 | 'Generate graphs history in {0}\nPlease wait...'.format( |
||
743 | self.glances_history.get_output_folder())) |
||
744 | self.display_popup( |
||
745 | 'Generate graphs history in {0}\nDone: {1} graphs generated'.format( |
||
746 | self.glances_history.get_output_folder(), |
||
747 | self.glances_history.generate_graph(stats))) |
||
748 | elif self.reset_history_tag and self.args.enable_history: |
||
749 | self.display_popup('Reset history') |
||
750 | self.glances_history.reset(stats) |
||
751 | elif (self.history_tag or self.reset_history_tag) and not self.args.enable_history: |
||
752 | try: |
||
753 | self.glances_history.graph_enabled() |
||
754 | except Exception: |
||
755 | self.display_popup('History disabled\nEnable it using --enable-history') |
||
756 | else: |
||
757 | self.display_popup('History disabled\nPlease install matplotlib') |
||
758 | self.history_tag = False |
||
759 | self.reset_history_tag = False |
||
760 | |||
761 | # Display edit filter popup |
||
762 | # Only in standalone mode (cs_status is None) |
||
763 | if self.edit_filter and cs_status is None: |
||
764 | new_filter = self.display_popup( |
||
765 | 'Process filter pattern: ', is_input=True, |
||
766 | input_value=glances_processes.process_filter) |
||
767 | glances_processes.process_filter = new_filter |
||
768 | elif self.edit_filter and cs_status != 'None': |
||
769 | self.display_popup('Process filter only available in standalone mode') |
||
770 | self.edit_filter = False |
||
771 | |||
772 | return True |
||
773 | |||
774 | def display_popup(self, message, |
||
775 | size_x=None, size_y=None, |
||
776 | duration=3, |
||
777 | is_input=False, |
||
778 | input_size=30, |
||
779 | input_value=None): |
||
780 | """ |
||
781 | Display a centered popup. |
||
782 | |||
783 | If is_input is False: |
||
784 | Display a centered popup with the given message during duration seconds |
||
785 | If size_x and size_y: set the popup size |
||
786 | else set it automatically |
||
787 | Return True if the popup could be displayed |
||
788 | |||
789 | If is_input is True: |
||
790 | Display a centered popup with the given message and a input field |
||
791 | If size_x and size_y: set the popup size |
||
792 | else set it automatically |
||
793 | Return the input string or None if the field is empty |
||
794 | """ |
||
795 | # Center the popup |
||
796 | sentence_list = message.split('\n') |
||
797 | if size_x is None: |
||
798 | size_x = len(max(sentence_list, key=len)) + 4 |
||
799 | # Add space for the input field |
||
800 | if is_input: |
||
801 | size_x += input_size |
||
802 | if size_y is None: |
||
803 | size_y = len(sentence_list) + 4 |
||
804 | screen_x = self.screen.getmaxyx()[1] |
||
805 | screen_y = self.screen.getmaxyx()[0] |
||
806 | if size_x > screen_x or size_y > screen_y: |
||
807 | # No size to display the popup => abord |
||
808 | return False |
||
809 | pos_x = int((screen_x - size_x) / 2) |
||
810 | pos_y = int((screen_y - size_y) / 2) |
||
811 | |||
812 | # Create the popup |
||
813 | popup = curses.newwin(size_y, size_x, pos_y, pos_x) |
||
814 | |||
815 | # Fill the popup |
||
816 | popup.border() |
||
817 | |||
818 | # Add the message |
||
819 | for y, m in enumerate(message.split('\n')): |
||
820 | popup.addnstr(2 + y, 2, m, len(m)) |
||
821 | |||
822 | if is_input and not WINDOWS: |
||
823 | # Create a subwindow for the text field |
||
824 | subpop = popup.derwin(1, input_size, 2, 2 + len(m)) |
||
825 | subpop.attron(self.colors_list['FILTER']) |
||
826 | # Init the field with the current value |
||
827 | if input_value is not None: |
||
828 | subpop.addnstr(0, 0, input_value, len(input_value)) |
||
829 | # Display the popup |
||
830 | popup.refresh() |
||
831 | subpop.refresh() |
||
832 | # Create the textbox inside the subwindows |
||
833 | self.set_cursor(2) |
||
834 | self.flash_cursor() |
||
835 | textbox = GlancesTextbox(subpop, insert_mode=False) |
||
836 | textbox.edit() |
||
837 | self.set_cursor(0) |
||
838 | self.no_flash_cursor() |
||
839 | if textbox.gather() != '': |
||
840 | logger.debug( |
||
841 | "User enters the following string: %s" % textbox.gather()) |
||
842 | return textbox.gather()[:-1] |
||
843 | else: |
||
844 | logger.debug("User centers an empty string") |
||
845 | return None |
||
846 | else: |
||
847 | # Display the popup |
||
848 | popup.refresh() |
||
849 | self.wait(duration * 1000) |
||
850 | return True |
||
851 | |||
852 | def display_plugin(self, plugin_stats, |
||
853 | display_optional=True, |
||
854 | display_additional=True, |
||
855 | max_y=65535): |
||
856 | """Display the plugin_stats on the screen. |
||
857 | |||
858 | If display_optional=True display the optional stats |
||
859 | If display_additional=True display additionnal stats |
||
860 | max_y do not display line > max_y |
||
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 | def erase(self): |
||
940 | """Erase the content of the screen.""" |
||
941 | self.term_window.erase() |
||
942 | |||
943 | def flush(self, stats, cs_status=None): |
||
944 | """Clear and update the screen. |
||
945 | |||
946 | stats: Stats database to display |
||
947 | cs_status: |
||
948 | "None": standalone or server mode |
||
949 | "Connected": Client is connected to the server |
||
950 | "Disconnected": Client is disconnected from the server |
||
951 | """ |
||
952 | self.erase() |
||
953 | self.display(stats, cs_status=cs_status) |
||
954 | |||
955 | def update(self, stats, cs_status=None, return_to_browser=False): |
||
956 | """Update the screen. |
||
957 | |||
958 | Wait for __refresh_time sec / catch key every 100 ms. |
||
959 | |||
960 | INPUT |
||
961 | stats: Stats database to display |
||
962 | cs_status: |
||
963 | "None": standalone or server mode |
||
964 | "Connected": Client is connected to the server |
||
965 | "Disconnected": Client is disconnected from the server |
||
966 | return_to_browser: |
||
967 | True: Do not exist, return to the browser list |
||
968 | False: Exit and return to the shell |
||
969 | |||
970 | OUPUT |
||
971 | True: Exit key has been pressed |
||
972 | False: Others cases... |
||
973 | """ |
||
974 | # Flush display |
||
975 | self.flush(stats, cs_status=cs_status) |
||
976 | |||
977 | # Wait |
||
978 | exitkey = False |
||
979 | countdown = Timer(self.__refresh_time) |
||
980 | while not countdown.finished() and not exitkey: |
||
981 | # Getkey |
||
982 | pressedkey = self.__catch_key(return_to_browser=return_to_browser) |
||
983 | # Is it an exit key ? |
||
984 | exitkey = (pressedkey == ord('\x1b') or pressedkey == ord('q')) |
||
985 | if not exitkey and pressedkey > -1: |
||
986 | # Redraw display |
||
987 | self.flush(stats, cs_status=cs_status) |
||
988 | # Wait 100ms... |
||
989 | self.wait() |
||
990 | |||
991 | return exitkey |
||
992 | |||
993 | def wait(self, delay=100): |
||
994 | """Wait delay in ms""" |
||
995 | curses.napms(100) |
||
996 | |||
997 | def get_stats_display_width(self, curse_msg, without_option=False): |
||
998 | """Return the width of the formatted curses message. |
||
999 | |||
1000 | The height is defined by the maximum line. |
||
1001 | """ |
||
1002 | try: |
||
1003 | if without_option: |
||
1004 | # Size without options |
||
1005 | c = len(max(''.join([(re.sub(r'[^\x00-\x7F]+', ' ', i['msg']) if not i['optional'] else "") |
||
1006 | for i in curse_msg['msgdict']]).split('\n'), key=len)) |
||
1007 | else: |
||
1008 | # Size with all options |
||
1009 | c = len(max(''.join([re.sub(r'[^\x00-\x7F]+', ' ', i['msg']) |
||
1010 | for i in curse_msg['msgdict']]).split('\n'), key=len)) |
||
1011 | except Exception: |
||
1012 | return 0 |
||
1013 | else: |
||
1014 | return c |
||
1015 | |||
1016 | def get_stats_display_height(self, curse_msg): |
||
1017 | r"""Return the height of the formatted curses message. |
||
1018 | |||
1019 | The height is defined by the number of '\n' (new line). |
||
1020 | """ |
||
1021 | try: |
||
1022 | c = [i['msg'] for i in curse_msg['msgdict']].count('\n') |
||
1023 | except Exception: |
||
1024 | return 0 |
||
1025 | else: |
||
1026 | return c + 1 |
||
1027 | |||
1028 | |||
1029 | class GlancesCursesStandalone(_GlancesCurses): |
||
1030 | |||
1031 | """Class for the Glances curse standalone.""" |
||
1032 | |||
1033 | pass |
||
1034 | |||
1035 | |||
1036 | class GlancesCursesClient(_GlancesCurses): |
||
1037 | |||
1038 | """Class for the Glances curse client.""" |
||
1039 | |||
1040 | pass |
||
1041 | |||
1042 | |||
1043 | if not WINDOWS: |
||
1044 | class GlancesTextbox(Textbox, object): |
||
1045 | |||
1046 | def __init__(self, *args, **kwargs): |
||
1047 | super(GlancesTextbox, self).__init__(*args, **kwargs) |
||
1048 | |||
1049 | def do_command(self, ch): |
||
1050 | if ch == 10: # Enter |
||
1051 | return 0 |
||
1052 | if ch == 127: # Back |
||
1053 | return 8 |
||
1054 | return super(GlancesTextbox, self).do_command(ch) |
||
1055 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.