_GlancesCurses._handle_diskio_latency()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
#
2
# This file is part of Glances.
3
#
4
# SPDX-FileCopyrightText: 2024 Nicolas Hennion <[email protected]>
5
#
6
# SPDX-License-Identifier: LGPL-3.0-only
7
#
8
9
"""Curses interface class."""
10
11
import functools
12
import getpass
13
import sys
14
15
from glances.events_list import glances_events
16
from glances.globals import MACOS, WINDOWS, disable, enable, nativestr, u
17
from glances.logger import logger
18
from glances.outputs.glances_colors import GlancesColors
19
from glances.outputs.glances_unicode import unicode_message
20
from glances.processes import glances_processes, sort_processes_stats_list
21
from glances.timer import Timer
22
23
# Import curses library for "normal" operating system
24
try:
25
    import curses
26
    import curses.panel
27
    from curses.textpad import Textbox
28
except ImportError:
29
    logger.critical("Curses module not found. Glances cannot start in standalone mode.")
30
    if WINDOWS:
31
        logger.critical("For Windows you can try installing windows-curses with pip install.")
32
    sys.exit(1)
33
34
35
class _GlancesCurses:
36
    """This class manages the curses display (and key pressed).
37
38
    Note: It is a private class, use GlancesCursesClient or GlancesCursesBrowser.
39
    """
40
41
    _hotkeys = {
42
        '\n': {'handler': '_handle_enter'},
43
        '0': {'switch': 'disable_irix'},
44
        '1': {'switch': 'percpu'},
45
        '2': {'switch': 'disable_left_sidebar'},
46
        '3': {'switch': 'disable_quicklook'},
47
        '4': {'handler': '_handle_quicklook'},
48
        '5': {'handler': '_handle_top_menu'},
49
        '6': {'switch': 'meangpu'},
50
        '/': {'switch': 'process_short_name'},
51
        'a': {'sort_key': 'auto'},
52
        'A': {'switch': 'disable_amps'},
53
        'b': {'switch': 'byte'},
54
        'B': {'handler': '_handle_diskio_iops'},
55
        'c': {'sort_key': 'cpu_percent'},
56
        'C': {'switch': 'disable_cloud'},
57
        'd': {'switch': 'disable_diskio'},
58
        'D': {'switch': 'disable_containers'},
59
        # 'e' > Enable/Disable process extended
60
        'E': {'handler': '_handle_erase_filter'},
61
        'f': {'handler': '_handle_fs_stats'},
62
        'F': {'switch': 'fs_free_space'},
63
        'g': {'switch': 'generate_graph'},
64
        'G': {'switch': 'disable_gpu'},
65
        'h': {'switch': 'help_tag'},
66
        'i': {'sort_key': 'io_counters'},
67
        'I': {'switch': 'disable_ip'},
68
        'j': {'switch': 'programs'},
69
        # 'k' > Kill selected process
70
        'K': {'switch': 'disable_connections'},
71
        'l': {'switch': 'disable_alert'},
72
        'L': {'handler': '_handle_diskio_latency'},
73
        'm': {'sort_key': 'memory_percent'},
74
        'M': {'switch': 'reset_minmax_tag'},
75
        'n': {'switch': 'disable_network'},
76
        'N': {'switch': 'disable_now'},
77
        'p': {'sort_key': 'name'},
78
        'P': {'switch': 'disable_ports'},
79
        # 'q' or ESCAPE > Quit
80
        'Q': {'switch': 'enable_irq'},
81
        'r': {'switch': 'disable_smart'},
82
        'R': {'switch': 'disable_raid'},
83
        's': {'switch': 'disable_sensors'},
84
        'S': {'switch': 'sparkline'},
85
        't': {'sort_key': 'cpu_times'},
86
        'T': {'switch': 'network_sum'},
87
        'u': {'sort_key': 'username'},
88
        'U': {'switch': 'network_cumul'},
89
        'V': {'switch': 'disable_vms'},
90
        'w': {'handler': '_handle_clean_logs'},
91
        'W': {'switch': 'disable_wifi'},
92
        'x': {'handler': '_handle_clean_critical_logs'},
93
        'z': {'handler': '_handle_disable_process'},
94
        '+': {'handler': '_handle_increase_nice'},
95
        '-': {'handler': '_handle_decrease_nice'},
96
        # "<" (shift + left arrow) navigation through process sort
97
        # ">" (shift + right arrow) navigation through process sort
98
        # "<" (left arrow) scroll through process name
99
        # ">" (right arrow) scroll through process name
100
        # 'UP' > Up in the server list
101
        # 'DOWN' > Down in the server list
102
    }
103
104
    _sort_loop = sort_processes_stats_list
105
106
    # Define top menu
107
    _top = ['quicklook', 'cpu', 'percpu', 'gpu', 'mem', 'memswap', 'load']
108
    _quicklook_max_width = 58
109
110
    # Define left sidebar
111
    # This variable is used in the make webui task in order to generate the
112
    # glances/outputs/static/js/uiconfig.json file for the web interface
113
    # This list can also be overwritten by the configuration file ([outputs] left_menu option)
114
    _left_sidebar = [
115
        'network',
116
        'ports',
117
        'wifi',
118
        'connections',
119
        'diskio',
120
        'fs',
121
        'irq',
122
        'folders',
123
        'raid',
124
        'smart',
125
        'sensors',
126
        'now',
127
    ]
128
    _left_sidebar_min_width = 23
129
    _left_sidebar_max_width = 34
130
131
    # Define right sidebar in a method because it depends of self.args.programs
132
    # See def _right_sidebar method
133
134
    def __init__(self, config=None, args=None):
135
        # Init
136
        self.config = config
137
        self.args = args
138
139
        # Init windows positions
140
        self.term_w = 80
141
        self.term_h = 24
142
143
        # Space between stats
144
        self.space_between_column = 3
145
        self.space_between_line = 2
146
147
        # Init the curses screen
148
        try:
149
            self.screen = curses.initscr()
150
            if not self.screen:
151
                logger.critical("Cannot init the curses library.\n")
152
                sys.exit(1)
153
            else:
154
                logger.debug(f"Curses library initialized with term: {curses.longname()}")
155
        except Exception as e:
156
            if args.export:
157
                logger.info("Cannot init the curses library, quiet mode on and export.")
158
                args.quiet = True
159
                return
160
161
            logger.critical(f"Cannot init the curses library ({e})")
162
            sys.exit(1)
163
164
        # Load configuration file
165
        self.load_config(config)
166
167
        # Init Curses cursor
168
        self._init_curses_cursor()
169
170
        # Init the colors
171
        self.colors_list = GlancesColors(args).get()
172
173
        # Init main window
174
        self.term_window = self.screen.subwin(0, 0)
175
176
        # Init edit filter tag
177
        self.edit_filter = False
178
179
        # Init nice increase/decrease tag
180
        self.increase_nice_process = False
181
        self.decrease_nice_process = False
182
183
        # Init kill process tag
184
        self.kill_process = False
185
186
        # Init the process min/max reset
187
        self.args.reset_minmax_tag = False
188
189
        # Init Glances cursor
190
        self.args.cursor_position = 0
191
        self.args.cursor_process_name_position = 0
192
193
        # For the moment cursor only available in standalone mode
194
        self.args.disable_cursor = not self.args.is_standalone
195
196
        # Catch key pressed with non blocking mode
197
        self.term_window.keypad(1)
198
        self.term_window.nodelay(1)
199
        self.pressedkey = -1
200
201
        # Is this the end ?
202
        self.is_end = False
203
204
        # History tag
205
        self._init_history()
206
207
    def load_config(self, config):
208
        """Load the outputs section of the configuration file."""
209
        if config is not None and config.has_section('outputs'):
210
            logger.debug('Read the outputs section in the configuration file')
211
            # Separator
212
            self.args.enable_separator = config.get_bool_value(
213
                'outputs', 'separator', default=self.args.enable_separator
214
            )
215
            # Set the left sidebar list
216
            self._left_sidebar = config.get_list_value('outputs', 'left_menu', default=self._left_sidebar)
217
            # Background color
218
            self.args.disable_bg = config.get_bool_value('outputs', 'disable_bg', default=self.args.disable_bg)
219
220
    def _right_sidebar(self):
221
        return [
222
            'vms',
223
            'containers',
224
            'processcount',
225
            'amps',
226
            'programlist' if self.args.programs else 'processlist',
227
            'alert',
228
        ]
229
230
    def _init_history(self):
231
        """Init the history option."""
232
233
        self.reset_history_tag = False
234
235
    def _init_curses_cursor(self):
236
        """Init cursors."""
237
238
        if hasattr(curses, 'noecho'):
239
            curses.noecho()
240
        if hasattr(curses, 'cbreak'):
241
            curses.cbreak()
242
        self.set_cursor(0)
243
244
    def set_cursor(self, value):
245
        """Configure the curse cursor appearance.
246
247
        0: invisible
248
        1: visible
249
        2: very visible
250
        """
251
        if hasattr(curses, 'curs_set'):
252
            try:
253
                curses.curs_set(value)
254
            except Exception:
255
                pass
256
257
    def get_key(self, window):
258
        return window.getch()
259
260
    def catch_actions_from_hotkey(self, hotkey):
261
        if self.pressedkey == ord(hotkey) and 'switch' in self._hotkeys[hotkey]:
262
            self._handle_switch(hotkey)
263
        elif self.pressedkey == ord(hotkey) and 'sort_key' in self._hotkeys[hotkey]:
264
            self._handle_sort_key(hotkey)
265
        if self.pressedkey == ord(hotkey) and 'handler' in self._hotkeys[hotkey]:
266
            action = getattr(self, self._hotkeys[hotkey]['handler'])
267
            action()
268
269
    def catch_other_actions_maybe_return_to_browser(self, return_to_browser):
270
        {
271
            self.pressedkey in {ord('e')} and not self.args.programs: self._handle_process_extended,
272
            self.pressedkey in {ord('k')} and not self.args.disable_cursor: self._handle_kill_process,
273
            self.pressedkey
274
            in {curses.KEY_LEFT if self.args.arrow_keys_sort else curses.KEY_SLEFT}: self._handle_sort_left,
275
            self.pressedkey
276
            in {curses.KEY_RIGHT if self.args.arrow_keys_sort else curses.KEY_SRIGHT}: self._handle_sort_right,
277
            self.pressedkey
278
            in {curses.KEY_SLEFT if self.args.arrow_keys_sort else curses.KEY_LEFT}: self._handle_process_name_left,
279
            self.pressedkey
280
            in {curses.KEY_SRIGHT if self.args.arrow_keys_sort else curses.KEY_RIGHT}: self._handle_process_name_right,
281
            self.pressedkey in {curses.KEY_UP, 65} and not self.args.disable_cursor: self._handle_cursor_up,
282
            self.pressedkey in {curses.KEY_DOWN, 66} and not self.args.disable_cursor: self._handle_cursor_down,
283
            self.pressedkey in {curses.KEY_F5, 18}: self._handle_refresh,
284
            self.pressedkey in {ord('\x1b'), ord('q')}: functools.partial(self._handle_quit, return_to_browser),
285
        }.get(True, lambda: None)()
286
287
    def __catch_key(self, return_to_browser=False):
288
        # Catch the pressed key
289
        self.pressedkey = self.get_key(self.term_window)
290
        if self.pressedkey == -1:
291
            return self.pressedkey
292
293
        # Actions (available in the global hotkey dict)...
294
        logger.debug(f"Keypressed (code: {self.pressedkey})")
295
        [self.catch_actions_from_hotkey(hotkey) for hotkey in self._hotkeys]
296
297
        # Other actions with key > 255 (ord will not work) and/or additional test...
298
        self.catch_other_actions_maybe_return_to_browser(return_to_browser)
299
300
        # Return the key code
301
        return self.pressedkey
302
303
    def _handle_switch(self, hotkey):
304
        option = '_'.join(self._hotkeys[hotkey]['switch'].split('_')[1:])
305
        if self._hotkeys[hotkey]['switch'].startswith('disable_'):
306
            if getattr(self.args, self._hotkeys[hotkey]['switch']):
307
                enable(self.args, option)
308
            else:
309
                disable(self.args, option)
310
        elif self._hotkeys[hotkey]['switch'].startswith('enable_'):
311
            if getattr(self.args, self._hotkeys[hotkey]['switch']):
312
                disable(self.args, option)
313
            else:
314
                enable(self.args, option)
315
        else:
316
            setattr(
317
                self.args,
318
                self._hotkeys[hotkey]['switch'],
319
                not getattr(self.args, self._hotkeys[hotkey]['switch']),
320
            )
321
322
    def _handle_sort_key(self, hotkey):
323
        glances_processes.set_sort_key(self._hotkeys[hotkey]['sort_key'], self._hotkeys[hotkey]['sort_key'] == 'auto')
324
325
    def _handle_enter(self):
326
        self.edit_filter = not self.edit_filter
327
328
    def _handle_quicklook(self):
329
        self.args.full_quicklook = not self.args.full_quicklook
330
        if self.args.full_quicklook:
331
            self.enable_fullquicklook()
332
        else:
333
            self.disable_fullquicklook()
334
335
    def _handle_top_menu(self):
336
        self.args.disable_top = not self.args.disable_top
337
        if self.args.disable_top:
338
            self.disable_top()
339
        else:
340
            self.enable_top()
341
342
    def _handle_process_extended(self):
343
        self.args.enable_process_extended = not self.args.enable_process_extended
344
        if not self.args.enable_process_extended:
345
            glances_processes.disable_extended()
346
        else:
347
            glances_processes.enable_extended()
348
        self.args.disable_cursor = self.args.enable_process_extended and self.args.is_standalone
349
350
    def _handle_erase_filter(self):
351
        glances_processes.process_filter = None
352
353
    def _handle_fs_stats(self):
354
        self.args.disable_fs = not self.args.disable_fs
355
        self.args.disable_folders = not self.args.disable_folders
356
357
    def _handle_increase_nice(self):
358
        self.increase_nice_process = not self.increase_nice_process
359
360
    def _handle_decrease_nice(self):
361
        self.decrease_nice_process = not self.decrease_nice_process
362
363
    def _handle_kill_process(self):
364
        self.kill_process = not self.kill_process
365
366
    def _handle_process_name_left(self):
367
        if self.args.cursor_process_name_position > 0:
368
            self.args.cursor_process_name_position -= 1
369
370
    def _handle_process_name_right(self):
371
        self.args.cursor_process_name_position += 1
372
373
    def _handle_clean_logs(self):
374
        glances_events.clean()
375
376
    def _handle_clean_critical_logs(self):
377
        glances_events.clean(critical=True)
378
379
    def _handle_disable_process(self):
380
        self.args.disable_process = not self.args.disable_process
381
        if self.args.disable_process:
382
            glances_processes.disable()
383
        else:
384
            glances_processes.enable()
385
386
    def _handle_diskio_iops(self):
387
        """Switch between bytes/s and IOPS for Disk IO."""
388
        self.args.diskio_iops = not self.args.diskio_iops
389
        if self.args.diskio_iops:
390
            self.args.diskio_latency = False
391
392
    def _handle_diskio_latency(self):
393
        """Switch between bytes/s and latency for Disk IO."""
394
        self.args.diskio_latency = not self.args.diskio_latency
395
        if self.args.diskio_latency:
396
            self.args.diskio_iops = False
397
398
    def _handle_sort_left(self):
399
        next_sort = (self.loop_position() - 1) % len(self._sort_loop)
400
        glances_processes.set_sort_key(self._sort_loop[next_sort], False)
401
402
    def _handle_sort_right(self):
403
        next_sort = (self.loop_position() + 1) % len(self._sort_loop)
404
        glances_processes.set_sort_key(self._sort_loop[next_sort], False)
405
406
    def _handle_cursor_up(self):
407
        if self.args.cursor_position > 0:
408
            self.args.cursor_position -= 1
409
410
    def _handle_cursor_down(self):
411
        if self.args.cursor_position < glances_processes.processes_count:
412
            self.args.cursor_position += 1
413
414
    def _handle_quit(self, return_to_browser):
415
        if return_to_browser:
416
            logger.info("Stop Glances client and return to the browser")
417
        else:
418
            logger.info(f"Stop Glances (keypressed: {self.pressedkey})")
419
            # End the curses window
420
            self.end()
421
            # Exit the program
422
            sys.exit(0)
423
424
    def _handle_refresh(self):
425
        glances_processes.reset_internal_cache()
426
427
    def loop_position(self):
428
        """Return the current sort in the loop"""
429
        for i, v in enumerate(self._sort_loop):
430
            if v == glances_processes.sort_key:
431
                return i
432
        return 0
433
434
    def disable_top(self):
435
        """Disable the top panel"""
436
        for p in ['quicklook', 'cpu', 'gpu', 'mem', 'memswap', 'load']:
437
            setattr(self.args, 'disable_' + p, True)
438
439
    def enable_top(self):
440
        """Enable the top panel"""
441
        for p in ['quicklook', 'cpu', 'gpu', 'mem', 'memswap', 'load']:
442
            setattr(self.args, 'disable_' + p, False)
443
444
    def disable_fullquicklook(self):
445
        """Disable the full quicklook mode"""
446
        for p in ['quicklook', 'cpu', 'gpu', 'mem', 'memswap']:
447
            setattr(self.args, 'disable_' + p, False)
448
449
    def enable_fullquicklook(self):
450
        """Disable the full quicklook mode"""
451
        self.args.disable_quicklook = False
452
        for p in ['cpu', 'gpu', 'mem', 'memswap']:
453
            setattr(self.args, 'disable_' + p, True)
454
455
    def end(self):
456
        """Shutdown the curses window."""
457
        if hasattr(curses, 'echo'):
458
            curses.echo()
459
        if hasattr(curses, 'nocbreak'):
460
            curses.nocbreak()
461
        try:
462
            curses.curs_set(1)
463
        except Exception:
464
            pass
465
        try:
466
            curses.endwin()
467
        except Exception:
468
            pass
469
        self.is_end = True
470
471
    def init_line_column(self):
472
        """Init the line and column position for the curses interface."""
473
        self.init_line()
474
        self.init_column()
475
476
    def init_line(self):
477
        """Init the line position for the curses interface."""
478
        self.line = 0
479
        self.next_line = 0
480
481
    def init_column(self):
482
        """Init the column position for the curses interface."""
483
        self.column = 0
484
        self.next_column = 0
485
486
    def new_line(self, separator=False):
487
        """New line in the curses interface."""
488
        self.line = self.next_line
489
490
    def new_column(self):
491
        """New column in the curses interface."""
492
        self.column = self.next_column
493
494
    def separator_line(self, color='SEPARATOR'):
495
        """Add a separator line in the curses interface."""
496
        if not self.args.enable_separator:
497
            return
498
        self.new_line()
499
        self.line -= 1
500
        line_width = self.term_window.getmaxyx()[1] - self.column
501
        if self.line >= 0 and self.line < self.term_window.getmaxyx()[0]:
502
            position = [self.line, self.column]
503
            line_color = self.colors_list[color]
504
            line_type = curses.ACS_HLINE if not self.args.disable_unicode else unicode_message('MEDIUM_LINE', self.args)
505
            self.term_window.hline(
506
                *position,
507
                line_type,
508
                line_width,
509
                line_color,
510
            )
511
512
    def __get_stat_display(self, stats, layer):
513
        """Return a dict of dict with all the stats display.
514
        # TODO: Drop extra parameter
515
516
        :param stats: Global stats dict
517
        :param layer: ~ 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
        :returns: dict of dict
524
            * key: plugin name
525
            * value: dict returned by the get_stats_display Plugin method
526
        """
527
        ret = {}
528
529
        for p in stats.getPluginsList(enable=False):
530
            # Ignore Quicklook because it is compute later in __display_top
531
            if p == 'quicklook':
532
                continue
533
534
            # Compute the plugin max size for the left sidebar
535
            plugin_max_width = None
536
            if p in self._left_sidebar:
537
                plugin_max_width = min(
538
                    self._left_sidebar_max_width,
539
                    max(self._left_sidebar_min_width, self.term_window.getmaxyx()[1] - 105),
540
                )
541
542
            # Get the view
543
            ret[p] = stats.get_plugin(p).get_stats_display(args=self.args, max_width=plugin_max_width)
544
545
        return ret
546
547
    def display(self, stats, cs_status=None):
548
        """Display stats on the screen.
549
550
        :param stats: Stats database to display
551
        :param cs_status:
552
            "None": standalone or server mode
553
            "Connected": Client is connected to a Glances server
554
            "SNMP": Client is connected to a SNMP server
555
            "Disconnected": Client is disconnected from the server
556
557
        :return: True if the stats have been displayed else False if the help have been displayed
558
        """
559
        # Init the internal line/column for Glances Curses
560
        self.init_line_column()
561
562
        # Update the stats messages
563
        ###########################
564
565
        # Get all the plugins view
566
        self.args.cs_status = cs_status
567
        __stat_display = self.__get_stat_display(stats, layer=cs_status)
568
569
        # Display the stats on the curses interface
570
        ###########################################
571
572
        # Help screen (on top of the other stats)
573
        if self.args.help_tag:
574
            # Display the stats...
575
            self.display_plugin(stats.get_plugin('help').get_stats_display(args=self.args))
576
            # ... and exit
577
            return False
578
579
        # =======================================
580
        # Display first line (system+ip+uptime)
581
        # Optionally: Cloud is on the second line
582
        # =======================================
583
        self.__display_header(__stat_display)
584
        self.separator_line()
585
586
        # ==============================================================
587
        # Display second line (<SUMMARY>+CPU|PERCPU+<GPU>+LOAD+MEM+SWAP)
588
        # ==============================================================
589
        self.__display_top(__stat_display, stats)
590
        self.init_column()
591
        self.separator_line()
592
593
        # ==================================================================
594
        # Display left sidebar (NETWORK+PORTS+DISKIO+FS+SENSORS+Current time)
595
        # ==================================================================
596
        self.__display_left(__stat_display)
597
598
        # ====================================
599
        # Display right stats (process and co)
600
        # ====================================
601
        self.__display_right(__stat_display)
602
603
        # =====================
604
        # Others popup messages
605
        # =====================
606
607
        # Display edit filter popup
608
        # Only in standalone mode (cs_status is None)
609
        if self.edit_filter and cs_status is None:
610
            new_filter = self.display_popup(
611
                'Process filter pattern: \n\n'
612
                + 'Examples:\n'
613
                + '- .*python.*\n'
614
                + '- /usr/lib.*\n'
615
                + '- name:.*nautilus.*\n'
616
                + '- cmdline:.*glances.*\n'
617
                + '- username:nicolargo\n'
618
                + '- username:^root        ',
619
                popup_type='input',
620
                input_value=glances_processes.process_filter_input,
621
            )
622
            glances_processes.process_filter = new_filter
623
        elif self.edit_filter and cs_status is not None:
624
            self.display_popup('Process filter only available in standalone mode')
625
        self.edit_filter = False
626
627
        # Manage increase/decrease nice level of the selected process
628
        # Only in standalone mode (cs_status is None)
629
        if self.increase_nice_process and cs_status is None:
630
            self.nice_increase(stats.get_plugin('processlist').get_raw()[self.args.cursor_position])
631
        self.increase_nice_process = False
632
        if self.decrease_nice_process and cs_status is None:
633
            self.nice_decrease(stats.get_plugin('processlist').get_raw()[self.args.cursor_position])
634
        self.decrease_nice_process = False
635
636
        # Display kill process confirmation popup
637
        # Only in standalone mode (cs_status is None)
638
        if self.kill_process and cs_status is None:
639
            self.kill(stats.get_plugin('processlist').get_raw()[self.args.cursor_position])
640
        elif self.kill_process and cs_status is not None:
641
            self.display_popup('Kill process only available for local processes')
642
        self.kill_process = False
643
644
        # Display graph generation popup
645
        if self.args.generate_graph:
646
            if 'graph' in stats.getExportsList():
647
                self.display_popup(f'Generate graph in {self.args.export_graph_path}')
648
            else:
649
                logger.warning('Graph export module is disable. Run Glances with --export graph to enable it.')
650
                self.args.generate_graph = False
651
652
        return True
653
654
    def nice_increase(self, process):
655
        glances_processes.nice_increase(process['pid'])
656
657
    def nice_decrease(self, process):
658
        glances_processes.nice_decrease(process['pid'])
659
660
    def kill(self, process):
661
        """Kill a process, or a list of process if the process has a childrens field.
662
663
        :param process
664
        :return: None
665
        """
666
        logger.debug(f"Selected process to kill: {process}")
667
668
        if 'childrens' in process:
669
            pid_to_kill = process['childrens']
670
        else:
671
            pid_to_kill = [process['pid']]
672
673
        confirm = self.display_popup(
674
            'Kill process: {} (pid: {}) ?\n\nConfirm ([y]es/[n]o): '.format(
675
                process['name'],
676
                ', '.join(map(str, pid_to_kill)),
677
            ),
678
            popup_type='yesno',
679
        )
680
681
        if confirm.lower().startswith('y'):
682
            for pid in pid_to_kill:
683
                try:
684
                    ret_kill = glances_processes.kill(pid)
685
                except Exception as e:
686
                    logger.error(f'Can not kill process {pid} ({e})')
687
                else:
688
                    logger.info(f'Kill signal has been sent to process {pid} (return code: {ret_kill})')
689
690
    def __display_header(self, stat_display):
691
        """Display the firsts lines (header) in the Curses interface.
692
693
        system + ip + uptime
694
        (cloud)
695
        """
696
        # First line
697
        self.new_line()
698
        self.space_between_column = 0
699
        l_uptime = 1
700
        for i in ['system', 'ip', 'uptime']:
701
            if i in stat_display:
702
                l_uptime += self.get_stats_display_width(stat_display[i])
703
        self.display_plugin(stat_display["system"], display_optional=(self.term_window.getmaxyx()[1] >= l_uptime))
704
        self.space_between_column = 3
705
        if 'ip' in stat_display:
706
            self.new_column()
707
            self.display_plugin(stat_display["ip"], display_optional=(self.term_window.getmaxyx()[1] >= 100))
708
        self.new_column()
709
        cloud_width = self.get_stats_display_width(stat_display.get("cloud", 0))
710
        self.display_plugin(stat_display["uptime"], add_space=-(cloud_width != 0))
711
        self.init_column()
712
        if cloud_width != 0:
713
            # Second line (optional)
714
            self.new_line()
715
            self.display_plugin(stat_display["cloud"])
716
717
    def __display_top(self, stat_display, stats):
718
        """Display the second line in the Curses interface.
719
720
        <QUICKLOOK> + CPU|PERCPU + <GPU> + MEM + SWAP + LOAD
721
        """
722
        self.init_column()
723
        self.new_line()
724
725
        # Init quicklook
726
        stat_display['quicklook'] = {'msgdict': []}
727
728
        # Dict for plugins width
729
        plugin_widths = {}
730
        for p in self._top:
731
            plugin_widths[p] = (
732
                self.get_stats_display_width(stat_display.get(p, 0)) if hasattr(self.args, 'disable_' + p) else 0
733
            )
734
735
        # Width of all plugins
736
        stats_width = sum(plugin_widths.values())
737
738
        # Number of plugin but quicklook
739
        stats_number = sum(
740
            [int(stat_display[p]['msgdict'] != []) for p in self._top if not getattr(self.args, 'disable_' + p)]
741
        )
742
743
        if not self.args.disable_quicklook:
744
            # Quick look is in the place !
745
            if self.args.full_quicklook:
746
                quicklook_width = self.term_window.getmaxyx()[1] - (
747
                    stats_width + 8 + stats_number * self.space_between_column
748
                )
749
            else:
750
                quicklook_width = min(
751
                    self.term_window.getmaxyx()[1] - (stats_width + 8 + stats_number * self.space_between_column),
752
                    self._quicklook_max_width - 5,
753
                )
754
            try:
755
                stat_display["quicklook"] = stats.get_plugin('quicklook').get_stats_display(
756
                    max_width=quicklook_width, args=self.args
757
                )
758
            except AttributeError as e:
759
                logger.debug(f"Quicklook plugin not available ({e})")
760
            else:
761
                plugin_widths['quicklook'] = self.get_stats_display_width(stat_display["quicklook"])
762
                stats_width = sum(plugin_widths.values()) + 1
763
            self.space_between_column = 1
764
            self.display_plugin(stat_display["quicklook"])
765
            self.new_column()
766
767
        # Compute spaces between plugins
768
        # Note: Only one space between Quicklook and others
769
        plugin_display_optional = {}
770
        for p in self._top:
771
            plugin_display_optional[p] = True
772
        if stats_number > 1:
773
            self.space_between_column = max(1, int((self.term_window.getmaxyx()[1] - stats_width) / (stats_number - 1)))
774
            for p in ['mem', 'cpu']:
775
                # No space ? Remove optional stats
776
                if self.space_between_column < 3:
777
                    plugin_display_optional[p] = False
778
                    plugin_widths[p] = (
779
                        self.get_stats_display_width(stat_display[p], without_option=True)
780
                        if hasattr(self.args, 'disable_' + p)
781
                        else 0
782
                    )
783
                    stats_width = sum(plugin_widths.values()) + 1
784
                    self.space_between_column = max(
785
                        1, int((self.term_window.getmaxyx()[1] - stats_width) / (stats_number - 1))
786
                    )
787
        else:
788
            self.space_between_column = 0
789
790
        # Display CPU, MEM, SWAP and LOAD
791
        for p in self._top:
792
            if p == 'quicklook':
793
                continue
794
            if p in stat_display:
795
                self.display_plugin(stat_display[p], display_optional=plugin_display_optional[p])
796
            if p != 'load':
797
                # Skip last column
798
                self.new_column()
799
800
        # Space between column
801
        self.space_between_column = 3
802
803
        # Backup line position
804
        self.saved_line = self.next_line
805
806
    def __display_left(self, stat_display):
807
        """Display the left sidebar in the Curses interface."""
808
        self.init_column()
809
810
        if self.args.disable_left_sidebar:
811
            return
812
813
        for p in self._left_sidebar:
814
            if (hasattr(self.args, 'enable_' + p) or hasattr(self.args, 'disable_' + p)) and p in stat_display:
815
                self.new_line()
816
                if p == 'sensors':
817
                    self.display_plugin(
818
                        stat_display['sensors'],
819
                        max_y=(self.term_window.getmaxyx()[0] - self.get_stats_display_height(stat_display['now']) - 2),
820
                    )
821
                else:
822
                    self.display_plugin(stat_display[p])
823
824
    def __display_right(self, stat_display):
825
        """Display the right sidebar in the Curses interface.
826
827
        docker + processcount + amps + processlist + alert
828
        """
829
        # Do not display anything if space is not available...
830
        if self.term_window.getmaxyx()[1] < self._left_sidebar_min_width:
831
            return
832
833
        # Restore line position
834
        self.next_line = self.saved_line
835
836
        # Display right sidebar
837
        self.new_column()
838
        for p in self._right_sidebar():
839
            if (hasattr(self.args, 'enable_' + p) or hasattr(self.args, 'disable_' + p)) and p in stat_display:
840
                self.new_line()
841
                if p in ['processlist', 'programlist']:
842
                    p_index = self._right_sidebar().index(p) + 1
843
                    self.display_plugin(
844
                        stat_display[p],
845
                        display_optional=(self.term_window.getmaxyx()[1] > 102),
846
                        display_additional=(not MACOS),
847
                        max_y=(
848
                            self.term_window.getmaxyx()[0]
849
                            - sum(
850
                                [
851
                                    self.get_stats_display_height(stat_display[i])
852
                                    for i in self._right_sidebar()[p_index:]
853
                                ]
854
                            )
855
                            - 2
856
                        ),
857
                    )
858
                else:
859
                    self.display_plugin(stat_display[p])
860
861
    def display_popup(
862
        self,
863
        message,
864
        size_x=None,
865
        size_y=None,
866
        duration=3,
867
        popup_type='info',
868
        input_size=30,
869
        input_value=None,
870
        is_password=False,
871
    ):
872
        """
873
        Display a centered popup.
874
875
        popup_type: ='info'
876
         Just an information popup, no user interaction
877
         Display a centered popup with the given message during duration seconds
878
         If size_x and size_y: set the popup size
879
         else set it automatically
880
         Return True if the popup could be displayed
881
882
        popup_type='input'
883
         Display a centered popup with the given message and a input field
884
         If size_x and size_y: set the popup size
885
         else set it automatically
886
         Return the input string or None if the field is empty
887
888
        popup_type='yesno'
889
         Display a centered popup with the given message
890
         If size_x and size_y: set the popup size
891
         else set it automatically
892
         Return True (yes) or False (no)
893
        """
894
        # Center the popup
895
        sentence_list = message.split('\n')
896
        if size_x is None:
897
            size_x = len(max(sentence_list, key=len)) + 4
898
            # Add space for the input field
899
            if popup_type == 'input':
900
                size_x += input_size
901
        if size_y is None:
902
            size_y = len(sentence_list) + 4
903
        screen_x = self.term_window.getmaxyx()[1]
904
        screen_y = self.term_window.getmaxyx()[0]
905
        if size_x > screen_x or size_y > screen_y:
906
            # No size to display the popup => abord
907
            return False
908
        pos_x = int((screen_x - size_x) / 2)
909
        pos_y = int((screen_y - size_y) / 2)
910
911
        # Create the popup
912
        popup = curses.newwin(size_y, size_x, pos_y, pos_x)
913
914
        # Fill the popup
915
        popup.border()
916
917
        # Add the message
918
        for y, m in enumerate(sentence_list):
919
            if m:
920
                popup.addnstr(2 + y, 2, m, len(m))
921
922
        if popup_type == 'info':
923
            # Display the popup
924
            popup.refresh()
925
            self.wait(duration * 1000)
926
            return True
927
928
        if popup_type == 'input':
929
            logger.info(popup_type)
930
            logger.info(is_password)
931
            # Create a sub-window for the text field
932
            sub_pop = popup.derwin(1, input_size, 2, 2 + len(m))
0 ignored issues
show
introduced by
The variable m does not seem to be defined in case the for loop on line 918 is not entered. Are you sure this can never be the case?
Loading history...
933
            sub_pop.attron(self.colors_list['FILTER'])
934
            # Init the field with the current value
935
            if input_value is not None:
936
                sub_pop.addnstr(0, 0, input_value, len(input_value))
937
            # Display the popup
938
            popup.refresh()
939
            sub_pop.refresh()
940
            # Create the textbox inside the sub-windows
941
            self.set_cursor(2)
942
            self.term_window.keypad(1)
943
            if is_password:
944
                textbox = getpass.getpass('')
945
                self.set_cursor(0)
946
                if textbox != '':
947
                    return textbox
948
                return None
949
950
            # No password
951
            textbox = GlancesTextbox(sub_pop, insert_mode=True)
952
            textbox.edit()
953
            self.set_cursor(0)
954
            if textbox.gather() != '':
955
                return textbox.gather()[:-1]
956
            return None
957
958
        if popup_type == 'yesno':
959
            # Create a sub-window for the text field
960
            sub_pop = popup.derwin(1, 2, len(sentence_list) + 1, len(m) + 2)
961
            sub_pop.attron(self.colors_list['FILTER'])
962
            # Init the field with the current value
963
            try:
964
                sub_pop.addnstr(0, 0, '', 0)
965
            except curses.error:
966
                pass
967
            # Display the popup
968
            popup.refresh()
969
            sub_pop.refresh()
970
            # Create the textbox inside the sub-windows
971
            self.set_cursor(2)
972
            self.term_window.keypad(1)
973
            textbox = GlancesTextboxYesNo(sub_pop, insert_mode=False)
974
            textbox.edit()
975
            self.set_cursor(0)
976
            # self.term_window.keypad(0)
977
            return textbox.gather()
978
979
        return None
980
981
    def setup_upper_left_pos(self, plugin_stats):
982
        screen_y, screen_x = self.term_window.getmaxyx()
983
984
        if plugin_stats['align'] == 'right':
985
            # Right align (last column)
986
            display_x = screen_x - self.get_stats_display_width(plugin_stats)
987
        else:
988
            display_x = self.column
989
990
        if plugin_stats['align'] == 'bottom':
991
            # Bottom (last line)
992
            display_y = screen_y - self.get_stats_display_height(plugin_stats)
993
        else:
994
            display_y = self.line
995
996
        return display_y, display_x
997
998
    def get_next_x_and_x_max(self, m, x, x_max):
999
        # New column
1000
        # Python 2: we need to decode to get real screen size because
1001
        # UTF-8 special tree chars occupy several bytes.
1002
        # Python 3: strings are strings and bytes are bytes, all is
1003
        # good.
1004
        try:
1005
            x += len(u(m['msg']))
1006
        except UnicodeDecodeError:
1007
            # Quick and dirty hack for issue #745
1008
            pass
1009
        if x > x_max:
1010
            x_max = x
1011
1012
        return x, x_max
1013
1014
    def display_stats_with_current_size(self, m, y, x):
1015
        screen_x = self.term_window.getmaxyx()[1]
1016
        self.term_window.addnstr(
1017
            y,
1018
            x,
1019
            m['msg'],
1020
            # Do not display outside the screen
1021
            screen_x - x,
1022
            self.colors_list[m['decoration']],
1023
        )
1024
1025
    def display_stats(self, plugin_stats, init, helper):
1026
        y, x, x_max = init
1027
        for m in plugin_stats['msgdict']:
1028
            # New line
1029
            try:
1030
                if m['msg'].startswith('\n'):
1031
                    y, x = helper['goto next, add first col'](y, x)
1032
                    continue
1033
            except Exception:
1034
                # Avoid exception (see issue #1692)
1035
                pass
1036
            # Do not display outside the screen
1037
            if x < 0:
1038
                continue
1039
            if helper['x overbound?'](m, x):
1040
                continue
1041
            if helper['y overbound?'](y):
1042
                break
1043
            # If display_optional = False do not display optional stats
1044
            if helper['display optional?'](m):
1045
                continue
1046
            # If display_additional = False do not display additional stats
1047
            if helper['display additional?'](m):
1048
                continue
1049
            # Is it possible to display the stat with the current screen size
1050
            # !!! Crash if not try/except... Why ???
1051
            try:
1052
                self.display_stats_with_current_size(m, y, x)
1053
            except Exception:
1054
                pass
1055
            else:
1056
                x, x_max = self.get_next_x_and_x_max(m, x, x_max)
1057
1058
        return y, x, x_max
1059
1060
    def display_plugin(self, plugin_stats, display_optional=True, display_additional=True, max_y=65535, add_space=0):
1061
        """Display the plugin_stats on the screen.
1062
1063
        :param plugin_stats:
1064
        :param display_optional: display the optional stats if True
1065
        :param display_additional: display additional stats if True
1066
        :param max_y: do not display line > max_y
1067
        :param add_space: add x space (line) after the plugin
1068
        """
1069
        # Exit if:
1070
        # - the plugin_stats message is empty
1071
        # - the display tag = False
1072
        if plugin_stats is None or not plugin_stats['msgdict'] or not plugin_stats['display']:
1073
            # Exit
1074
            return 0
1075
1076
        # Get the screen size
1077
        screen_y, screen_x = self.term_window.getmaxyx()
1078
1079
        # Set the upper/left position of the message
1080
        display_y, display_x = self.setup_upper_left_pos(plugin_stats)
1081
1082
        helper = {
1083
            'goto next, add first col': lambda y, x: (y + 1, display_x),
0 ignored issues
show
introduced by
The variable display_x does not seem to be defined for all execution paths.
Loading history...
1084
            'x overbound?': lambda m, x: not m['splittable'] and (x + len(m['msg']) > screen_x),
0 ignored issues
show
introduced by
The variable screen_x does not seem to be defined for all execution paths.
Loading history...
1085
            'y overbound?': lambda y: y < 0 or (y + 1 > screen_y) or (y > max_y),
0 ignored issues
show
introduced by
The variable screen_y does not seem to be defined for all execution paths.
Loading history...
1086
            'display optional?': lambda m: not display_optional and m['optional'],
1087
            'display additional?': lambda m: not display_additional and m['additional'],
1088
        }
1089
1090
        # Display
1091
        init = display_y, display_x, display_x
1092
        y, x, x_max = self.display_stats(plugin_stats, init, helper)
1093
1094
        # Compute the next Glances column/line position
1095
        self.next_column = max(self.next_column, x_max + self.space_between_column)
1096
        self.next_line = max(self.next_line, y + self.space_between_line)
1097
1098
        # Have empty lines after the plugins
1099
        self.next_line += add_space
1100
        return None
1101
1102
    def clear(self):
1103
        """Erase the content of the screen.
1104
        The difference is that clear() also calls clearok(). clearok()
1105
        basically tells ncurses to forget whatever it knows about the current
1106
        terminal contents, so that when refresh() is called, it will actually
1107
        begin by clearing the entire terminal screen before redrawing any of it."""
1108
        self.term_window.clear()
1109
1110
    def erase(self):
1111
        """Erase the content of the screen.
1112
        erase() on the other hand, just clears the screen (the internal
1113
        object, not the terminal screen). When refresh() is later called,
1114
        ncurses will still compute the minimum number of characters to send to
1115
        update the terminal."""
1116
        self.term_window.erase()
1117
1118
    def refresh(self):
1119
        """Refresh the windows"""
1120
        self.term_window.refresh()
1121
1122
    def flush(self, stats, cs_status=None):
1123
        """Erase and update the screen.
1124
1125
        :param stats: Stats database to display
1126
        :param cs_status:
1127
            "None": standalone or server mode
1128
            "Connected": Client is connected to the server
1129
            "Disconnected": Client is disconnected from the server
1130
        """
1131
        # See https://stackoverflow.com/a/43486979/1919431
1132
        self.erase()
1133
        self.display(stats, cs_status=cs_status)
1134
        self.refresh()
1135
1136
    def update(self, stats, duration=3, cs_status=None, return_to_browser=False):
1137
        """Update the screen.
1138
1139
        :param stats: Stats database to display
1140
        :param duration: duration of the loop
1141
        :param cs_status:
1142
            "None": standalone or server mode
1143
            "Connected": Client is connected to the server
1144
            "Disconnected": Client is disconnected from the server
1145
        :param return_to_browser:
1146
            True: Do not exist, return to the browser list
1147
            False: Exit and return to the shell
1148
1149
        :return: True if exit key has been pressed else False
1150
        """
1151
        # Flush display
1152
        self.flush(stats, cs_status=cs_status)
1153
1154
        # If the duration is < 0 (update + export time > refresh_time)
1155
        # Then display the interface and log a message
1156
        if duration <= 0:
1157
            logger.warning('Update and export time higher than refresh_time.')
1158
            duration = 0.1
1159
1160
        # Wait duration (in s) time
1161
        isexitkey = False
1162
        countdown = Timer(duration)
1163
        # Set the default timeout (in ms) between two getch
1164
        self.term_window.timeout(100)
1165
        while not countdown.finished() and not isexitkey:
1166
            # Getkey
1167
            pressedkey = self.__catch_key(return_to_browser=return_to_browser)
1168
1169
            if pressedkey == -1:
1170
                self.wait()
1171
                continue
1172
1173
            isexitkey = pressedkey == ord('\x1b') or pressedkey == ord('q')
1174
1175
            if pressedkey == curses.KEY_F5 or self.pressedkey == 18:
1176
                # Were asked to refresh (F5 or Ctrl-R)
1177
                self.clear()
1178
                return isexitkey
1179
1180
            if pressedkey in (curses.KEY_UP, 65, curses.KEY_DOWN, 66, curses.KEY_LEFT, 68, curses.KEY_RIGHT, 67):
1181
                # Up of won key pressed, reset the countdown
1182
                # Better for user experience
1183
                countdown.reset()
1184
1185
            if isexitkey and self.args.help_tag:
1186
                # Quit from help should return to main screen, not exit #1874
1187
                self.args.help_tag = not self.args.help_tag
1188
                return False
1189
1190
            if not isexitkey and pressedkey > -1:
1191
                # Redraw display
1192
                self.flush(stats, cs_status=cs_status)
1193
                # Overwrite the timeout with the countdown
1194
                self.wait(delay=int(countdown.get() * 1000))
1195
1196
        return isexitkey
1197
1198
    def wait(self, delay=100):
1199
        """Wait delay in ms"""
1200
        curses.napms(delay)
1201
1202
    def get_stats_display_width(self, curse_msg, without_option=False):
1203
        """Return the width of the formatted curses message."""
1204
        try:
1205
            if without_option:
1206
                # Size without options
1207
                c = len(
1208
                    max(
1209
                        ''.join(
1210
                            [
1211
                                (u(u(nativestr(i['msg'])).encode('ascii', 'replace')) if not i['optional'] else "")
1212
                                for i in curse_msg['msgdict']
1213
                            ]
1214
                        ).split('\n'),
1215
                        key=len,
1216
                    )
1217
                )
1218
            else:
1219
                # Size with all options
1220
                c = len(
1221
                    max(
1222
                        ''.join(
1223
                            [u(u(nativestr(i['msg'])).encode('ascii', 'replace')) for i in curse_msg['msgdict']]
1224
                        ).split('\n'),
1225
                        key=len,
1226
                    )
1227
                )
1228
        except Exception as e:
1229
            logger.debug(f'ERROR: Can not compute plugin width ({e})')
1230
            return 0
1231
        else:
1232
            return c
1233
1234
    def get_stats_display_height(self, curse_msg):
1235
        """Return the height of the formatted curses message.
1236
1237
        The height is defined by the number of '\n' (new line).
1238
        """
1239
        try:
1240
            c = [i['msg'] for i in curse_msg['msgdict']].count('\n')
1241
        except Exception as e:
1242
            logger.debug(f'ERROR: Can not compute plugin height ({e})')
1243
            return 0
1244
        else:
1245
            return c + 1
1246
1247
1248
class GlancesCursesStandalone(_GlancesCurses):
1249
    """Class for the Glances curse standalone."""
1250
1251
    # Default number of processes to displayed is set to 50
1252
    glances_processes.max_processes = 50
1253
1254
1255
class GlancesCursesClient(_GlancesCurses):
1256
    """Class for the Glances curse client."""
1257
1258
    # Default number of processes to displayed is set to 50
1259
    # For the moment, cursor in client/server mode is not supported see #3221
1260
    glances_processes.max_processes = 50
1261
1262
1263
class GlancesTextbox(Textbox):
1264
    def __init__(self, *args, **kwargs):
1265
        super().__init__(*args, **kwargs)
1266
1267
    def do_command(self, ch):
1268
        if ch == 10:  # Enter
1269
            return 0
1270
        if ch == 127:  # Back
1271
            return 8
1272
        return super().do_command(ch)
1273
1274
1275
class GlancesTextboxYesNo(Textbox):
1276
    def __init__(self, *args, **kwargs):
1277
        super().__init__(*args, **kwargs)
1278
1279
    def do_command(self, ch):
1280
        return super().do_command(ch)
1281