Test Failed
Push — develop ( d7cf39...faa4bd )
by Nicolas
04:34 queued 10s
created

glances/outputs/glances_curses.py (11 issues)

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:
0 ignored issues
show
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
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)
210
            else:
211
                curses.init_pair(1, curses.COLOR_WHITE, -1)
212
            if self.args.disable_bg:
213
                curses.init_pair(2, curses.COLOR_RED, -1)
214
                curses.init_pair(3, curses.COLOR_GREEN, -1)
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:
0 ignored issues
show
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
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:
0 ignored issues
show
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
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:
0 ignored issues
show
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
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)
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
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:
0 ignored issues
show
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
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)
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
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(
0 ignored issues
show
Coding Style Best Practice introduced by
Specify string format arguments as logging function parameters
Loading history...
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:
0 ignored issues
show
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
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:
0 ignored issues
show
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
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:
0 ignored issues
show
Catching very general exceptions such as Exception is usually not recommended.

Generally, you would want to handle very specific errors in the exception handler. This ensure that you do not hide other types of errors which should be fixed.

So, unless you specifically plan to handle any error, consider adding a more specific exception.

Loading history...
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