Test Failed
Push — master ( ce0fc3...e09530 )
by Nicolas
03:36
created

PluginModel._msg_curse_header()   F

Complexity

Conditions 25

Size

Total Lines 63
Code Lines 52

Duplication

Lines 63
Ratio 100 %

Importance

Changes 0
Metric Value
cc 25
eloc 52
nop 4
dl 63
loc 63
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like glances.plugins.processlist.PluginModel._msg_curse_header() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
"""Process list plugin."""
10
11
import copy
12
import functools
13
import os
14
15
from glances.globals import WINDOWS, key_exist_value_not_none_not_v, replace_special_chars
16
from glances.logger import logger
17
from glances.outputs.glances_unicode import unicode_message
18
from glances.plugins.core import PluginModel as CorePluginModel
19
from glances.plugins.plugin.model import GlancesPluginModel
20
from glances.processes import glances_processes, sort_stats
21
22
# Fields description
23
# description: human readable description
24
# short_name: shortname to use un UI
25
# unit: unit type
26
# rate: if True then compute and add *_gauge and *_rate_per_is fields
27
# min_symbol: Auto unit should be used if value > than 1 'X' (K, M, G)...
28
fields_description = {
29
    'pid': {
30
        'description': 'Process identifier (ID)',
31
        'unit': 'number',
32
    },
33
    'name': {
34
        'description': 'Process name',
35
        'unit': 'string',
36
    },
37
    'cmdline': {
38
        'description': 'Command line with arguments',
39
        'unit': 'list',
40
    },
41
    'username': {
42
        'description': 'Process owner',
43
        'unit': 'string',
44
    },
45
    'num_threads': {
46
        'description': 'Number of threads',
47
        'unit': 'number',
48
    },
49
    'cpu_percent': {
50
        'description': 'Process CPU consumption',
51
        'unit': 'percent',
52
    },
53
    'memory_percent': {
54
        'description': 'Process memory consumption',
55
        'unit': 'percent',
56
    },
57
    'memory_info': {
58
        'description': 'Process memory information (dict with rss, vms, shared, text, lib, data, dirty keys)',
59
        'unit': 'byte',
60
    },
61
    'status': {
62
        'description': 'Process status',
63
        'unit': 'string',
64
    },
65
    'nice': {
66
        'description': 'Process nice value',
67
        'unit': 'number',
68
    },
69
    'cpu_times': {
70
        'description': 'Process CPU times (dict with user, system, iowait keys)',
71
        'unit': 'second',
72
    },
73
    'gids': {
74
        'description': 'Process group IDs (dict with real, effective, saved keys)',
75
        'unit': 'number',
76
    },
77
    'io_counters': {
78
        'description': 'Process IO counters (list with read_count, write_count, read_bytes, write_bytes, io_tag keys)',
79
        'unit': 'byte',
80
    },
81
}
82
83
84
def seconds_to_hms(input_seconds):
85
    """Convert seconds to human-readable time."""
86
    minutes, seconds = divmod(input_seconds, 60)
87
    hours, minutes = divmod(minutes, 60)
88
89
    hours = int(hours)
90
    minutes = int(minutes)
91
    seconds = str(int(seconds)).zfill(2)
92
93
    return hours, minutes, seconds
94
95
96
def split_cmdline(bare_process_name, cmdline):
97
    """Return path, cmd and arguments for a process cmdline based on bare_process_name.
98
99
    If first argument of cmdline starts with the bare_process_name then
100
    cmdline will just be considered cmd and path will be empty (see https://github.com/nicolargo/glances/issues/1795)
101
102
    :param bare_process_name: Name of the process from psutil
103
    :param cmdline: cmdline from psutil
104
    :return: Tuple with three strings, which are path, cmd and arguments of the process
105
    """
106
    if cmdline[0].startswith(bare_process_name):
107
        path, cmd = "", cmdline[0]
108
    else:
109
        path, cmd = os.path.split(cmdline[0])
110
    arguments = ' '.join(cmdline[1:])
111
    return path, cmd, arguments
112
113
114
class PluginModel(GlancesPluginModel):
115
    """Glances' processes plugin.
116
117
    stats is a list
118
    """
119
120
    # Default list of processes stats to be grabbed / displayed
121
    # Can be altered by glances_processes.disable_stats
122
    enable_stats = [
123
        'cpu_percent',
124
        'memory_percent',
125
        'memory_info',  # vms and rss
126
        'pid',
127
        'username',
128
        'cpu_times',
129
        'num_threads',
130
        'nice',
131
        'status',
132
        'io_counters',  # ior and iow
133
        'cmdline',
134
    ]
135
136
    # Define the header layout of the processes list columns
137
    layout_header = {
138
        'cpu': '{:<6} ',
139
        'mem': '{:<5} ',
140
        'virt': '{:<5} ',
141
        'res': '{:<5} ',
142
        'pid': '{:>{width}} ',
143
        'user': '{:<10} ',
144
        'time': '{:>8} ',
145
        'thread': '{:<3} ',
146
        'nice': '{:>3} ',
147
        'status': '{:>1} ',
148
        'ior': '{:>4} ',
149
        'iow': '{:<4} ',
150
        'command': '{} {}',
151
    }
152
153
    # Define the stat layout of the processes list columns
154
    layout_stat = {
155
        'cpu': '{:<6.1f}',
156
        'cpu_no_digit': '{:<6.0f}',
157
        'mem': '{:<5.1f} ',
158
        'virt': '{:<5} ',
159
        'res': '{:<5} ',
160
        'pid': '{:>{width}} ',
161
        'user': '{:<10} ',
162
        'time': '{:>8} ',
163
        'thread': '{:<3} ',
164
        'nice': '{:>3} ',
165
        'status': '{:>1} ',
166
        'ior': '{:>4} ',
167
        'iow': '{:<4} ',
168
        'command': '{}',
169
        'name': '[{}]',
170
    }
171
172
    def __init__(self, args=None, config=None):
173
        """Init the plugin."""
174
        super().__init__(args=args, config=config, fields_description=fields_description, stats_init_value=[])
175
176
        # We want to display the stat in the curse interface
177
        self.display_curse = True
178
179
        # Trying to display proc time
180
        self.tag_proc_time = True
181
182
        # Call CorePluginModel to get the core number (needed when not in IRIX mode / Solaris mode)
183
        try:
184
            self.nb_log_core = CorePluginModel(args=self.args).update()["log"]
185
        except Exception:
186
            self.nb_log_core = 0
187
188
        # Get the max values (dict)
189
        self.max_values = copy.deepcopy(glances_processes.max_values())
190
191
        # Get the maximum PID number
192
        # Use to optimize space (see https://github.com/nicolargo/glances/issues/959)
193
        self.pid_max = glances_processes.pid_max
194
195
        # Load the config file
196
        self.load(args, config)
197
198
        # The default sort key could also be overwrite by command line (see #1903)
199
        if args and args.sort_processes_key is not None:
200
            glances_processes.set_sort_key(args.sort_processes_key, False)
201
202
        # Note: 'glances_processes' is already init in the processes.py script
203
204
    def load(self, args, config):
205
        # Set the default sort key if it is defined in the configuration file
206
        if config is None or 'processlist' not in config.as_dict():
207
            return
208
        if 'sort_key' in config.as_dict()['processlist']:
209
            logger.debug(
210
                'Configuration overwrites processes sort key by {}'.format(config.as_dict()['processlist']['sort_key'])
211
            )
212
            glances_processes.set_sort_key(config.as_dict()['processlist']['sort_key'], False)
213
        if 'export' in config.as_dict()['processlist']:
214
            glances_processes.export_process_filter = config.as_dict()['processlist']['export']
215
            if args.export:
216
                logger.info("Export process filter is set to: {}".format(config.as_dict()['processlist']['export']))
217
        if 'disable_stats' in config.as_dict()['processlist']:
218
            logger.info(
219
                'Followings processes stats wil not be displayed: {}'.format(
220
                    config.as_dict()['processlist']['disable_stats']
221
                )
222
            )
223
            glances_processes.disable_stats = config.as_dict()['processlist']['disable_stats'].split(',')
224
225
    def get_key(self):
226
        """Return the key of the list."""
227
        return 'pid'
228
229
    def update(self):
230
        """Update processes stats using the input method."""
231
        # Update the stats
232
        if self.input_method == 'local':
233
            # Update stats using the standard system lib
234
            # Note: Update is done in the processcount plugin
235
            # Just return the result
236
            stats = glances_processes.get_list()
237
        else:
238
            stats = self.get_init_value()
239
240
        # Get the max values (dict)
241
        # Use Deep copy to avoid change between update and display
242
        self.max_values = copy.deepcopy(glances_processes.max_values())
243
244
        # Update the stats
245
        self.stats = stats
246
247
        return self.stats
248
249
    def get_export(self):
250
        """Return the processes list to export.
251
        Not all the processeses are exported.
252
        Only the one defined in the Glances configuration file (see #794 for details).
253
        """
254
        return glances_processes.get_export()
255
256
    def get_nice_alert(self, value):
257
        """Return the alert relative to the Nice configuration list"""
258
        value = str(value)
259
        if self.get_limit('nice_critical') and value in self.get_limit('nice_critical'):
260
            return 'CRITICAL'
261
        if self.get_limit('nice_warning') and value in self.get_limit('nice_warning'):
262
            return 'WARNING'
263
        if self.get_limit('nice_careful') and value in self.get_limit('nice_careful'):
264
            return 'CAREFUL'
265
266
        return 'DEFAULT'
267
268
    def _get_process_curses_cpu_percent(self, p, selected, args):
269
        """Return process CPU curses"""
270
        if key_exist_value_not_none_not_v('cpu_percent', p, ''):
271
            cpu_layout = self.layout_stat['cpu'] if p['cpu_percent'] < 100 else self.layout_stat['cpu_no_digit']
272
            if args.disable_irix and self.nb_log_core != 0:
273
                msg = cpu_layout.format(p['cpu_percent'] / float(self.nb_log_core))
274
            else:
275
                msg = cpu_layout.format(p['cpu_percent'])
276
            alert = self.get_alert(
277
                p['cpu_percent'],
278
                highlight_zero=False,
279
                is_max=(p['cpu_percent'] == self.max_values['cpu_percent']),
280
                header="cpu",
281
            )
282
            ret = self.curse_add_line(msg, alert)
283
        else:
284
            msg = self.layout_header['cpu'].format('?')
285
            ret = self.curse_add_line(msg)
286
        return ret
287
288
    def _get_process_curses_memory_percent(self, p, selected, args):
289
        """Return process MEM curses"""
290
        if key_exist_value_not_none_not_v('memory_percent', p, ''):
291
            msg = self.layout_stat['mem'].format(p['memory_percent'])
292
            alert = self.get_alert(
293
                p['memory_percent'],
294
                highlight_zero=False,
295
                is_max=(p['memory_percent'] == self.max_values['memory_percent']),
296
                header="mem",
297
            )
298
            ret = self.curse_add_line(msg, alert)
299
        else:
300
            msg = self.layout_header['mem'].format('?')
301
            ret = self.curse_add_line(msg)
302
        return ret
303
304
    def _get_process_curses_vms(self, p, selected, args):
305
        """Return process VMS curses"""
306
        if key_exist_value_not_none_not_v('memory_info', p, '', 1) and 'vms' in p['memory_info']:
307
            msg = self.layout_stat['virt'].format(self.auto_unit(p['memory_info']['vms'], low_precision=False))
308
            ret = self.curse_add_line(msg, optional=True)
309
        else:
310
            msg = self.layout_header['virt'].format('?')
311
            ret = self.curse_add_line(msg)
312
        return ret
313
314
    def _get_process_curses_rss(self, p, selected, args):
315
        """Return process RSS curses"""
316
        if key_exist_value_not_none_not_v('memory_info', p, '', 0) and 'rss' in p['memory_info']:
317
            msg = self.layout_stat['res'].format(self.auto_unit(p['memory_info']['rss'], low_precision=False))
318
            ret = self.curse_add_line(msg, optional=True)
319
        else:
320
            msg = self.layout_header['res'].format('?')
321
            ret = self.curse_add_line(msg)
322
        return ret
323
324
    def _get_process_curses_memory_info(self, p, selected, args):
325
        return [
326
            self._get_process_curses_vms(p, selected, args),
327
            self._get_process_curses_rss(p, selected, args),
328
        ]
329
330
    def _get_process_curses_pid(self, p, selected, args):
331
        """Return process PID curses"""
332
        # Display processes, so the PID should be displayed
333
        msg = self.layout_stat['pid'].format(p['pid'], width=self._max_pid_size())
334
        return self.curse_add_line(msg)
335
336
    def _get_process_curses_username(self, p, selected, args):
337
        """Return process username curses"""
338
        if 'username' in p:
339
            # docker internal users are displayed as ints only, therefore str()
340
            # Correct issue #886 on Windows OS
341
            msg = self.layout_stat['user'].format(str(p['username'])[:9])
342
        else:
343
            msg = self.layout_header['user'].format('?')
344
        return self.curse_add_line(msg)
345
346
    def _get_process_curses_cpu_times(self, p, selected, args):
347
        """Return process time curses"""
348
        cpu_times = p['cpu_times']
349
        try:
350
            # Sum user and system time
351
            user_system_time = cpu_times['user'] + cpu_times['system']
352
        except (OverflowError, TypeError, KeyError):
353
            # Catch OverflowError on some Amazon EC2 server
354
            # See https://github.com/nicolargo/glances/issues/87
355
            # Also catch TypeError on macOS
356
            # See: https://github.com/nicolargo/glances/issues/622
357
            # Also catch KeyError (as no stats be present for processes of other users)
358
            # See: https://github.com/nicolargo/glances/issues/2831
359
            # logger.debug("Cannot get TIME+ ({})".format(e))
360
            msg = self.layout_header['time'].format('?')
361
            return self.curse_add_line(msg, optional=True)
362
363
        hours, minutes, seconds = seconds_to_hms(user_system_time)
364
        if hours > 99:
365
            msg = f'{hours:<7}h'
366
        elif 0 < hours < 100:
367
            msg = f'{hours}h{minutes}:{seconds}'
368
        else:
369
            msg = f'{minutes}:{seconds}'
370
371
        msg = self.layout_stat['time'].format(msg)
372
        if hours > 0:
373
            return self.curse_add_line(msg, decoration='CPU_TIME', optional=True)
374
375
        return self.curse_add_line(msg, optional=True)
376
377
    def _get_process_curses_num_threads(self, p, selected, args):
378
        """Return process thread curses"""
379
        if 'num_threads' in p:
380
            num_threads = p['num_threads']
381
            if num_threads is None:
382
                num_threads = '?'
383
            msg = self.layout_stat['thread'].format(num_threads)
384
        else:
385
            msg = self.layout_header['thread'].format('?')
386
        return self.curse_add_line(msg)
387
388
    def _get_process_curses_nice(self, p, selected, args):
389
        """Return process nice curses"""
390
        if 'nice' in p:
391
            nice = p['nice']
392
            if nice is None:
393
                nice = '?'
394
            msg = self.layout_stat['nice'].format(nice)
395
            ret = self.curse_add_line(msg, decoration=self.get_nice_alert(nice))
396
        else:
397
            msg = self.layout_header['nice'].format('?')
398
            ret = self.curse_add_line(msg)
399
        return ret
400
401
    def _get_process_curses_status(self, p, selected, args):
402
        """Return process status curses"""
403
        if 'status' in p:
404
            status = p['status']
405
            msg = self.layout_stat['status'].format(status)
406
            if status == 'R':
407
                ret = self.curse_add_line(msg, decoration='STATUS')
408
            else:
409
                ret = self.curse_add_line(msg)
410
        else:
411
            msg = self.layout_header['status'].format('?')
412
            ret = self.curse_add_line(msg)
413
        return ret
414
415
    def _get_process_curses_io_read_write(self, p, selected, args, rorw='ior'):
416
        """Return process IO Read or Write curses"""
417
        if 'io_counters' in p and p['io_counters'][4] == 1 and p['time_since_update'] != 0:
418
            # Display rate if stats is available and io_tag ([4]) == 1
419
            # IO
420
            io = int(
421
                (p['io_counters'][0 if rorw == 'ior' else 1] - p['io_counters'][2 if rorw == 'ior' else 3])
422
                / p['time_since_update']
423
            )
424
            if io == 0:
425
                msg = self.layout_stat[rorw].format("0")
426
            else:
427
                msg = self.layout_stat[rorw].format(self.auto_unit(io, low_precision=True))
428
            ret = self.curse_add_line(msg, optional=True, additional=True)
429
        else:
430
            msg = self.layout_header[rorw].format("?")
431
            ret = self.curse_add_line(msg, optional=True, additional=True)
432
        return ret
433
434
    def _get_process_curses_io_counters(self, p, selected, args):
435
        return [
436
            self._get_process_curses_io_read_write(p, selected, args, rorw='ior'),
437
            self._get_process_curses_io_read_write(p, selected, args, rorw='iow'),
438
        ]
439
440
    def _get_process_curses_cmdline(self, p, selected, args):
441
        """Return process cmdline curses"""
442
        ret = []
443
        # If no command line for the process is available, fallback to the bare process name instead
444
        bare_process_name = p['name']
445
        cmdline = p.get('cmdline', '?')
446
        try:
447
            process_decoration = 'PROCESS_SELECTED' if (selected and not args.disable_cursor) else 'PROCESS'
448
            if cmdline:
449
                path, cmd, arguments = split_cmdline(bare_process_name, cmdline)
450
                # Manage end of line in arguments (see #1692)
451
                arguments = replace_special_chars(arguments)
452
                if os.path.isdir(path) and not args.process_short_name:
453
                    msg = self.layout_stat['command'].format(path) + os.sep
454
                    ret.append(self.curse_add_line(msg, splittable=True))
455
                    ret.append(self.curse_add_line(cmd, decoration=process_decoration, splittable=True))
456
                else:
457
                    msg = self.layout_stat['command'].format(cmd)
458
                    ret.append(self.curse_add_line(msg, decoration=process_decoration, splittable=True))
459
                if arguments:
460
                    msg = ' ' + self.layout_stat['command'].format(arguments)
461
                    ret.append(self.curse_add_line(msg, splittable=True))
462
            else:
463
                msg = self.layout_stat['name'].format(bare_process_name)
464
                ret.append(self.curse_add_line(msg, decoration=process_decoration, splittable=True))
465
        except (TypeError, UnicodeEncodeError) as e:
466
            # Avoid crash after running fine for several hours #1335
467
            logger.debug(f"Can not decode command line '{cmdline}' ({e})")
468
            ret.append(self.curse_add_line('', splittable=True))
469
        return ret
470
471
    def get_process_curses_data(self, p, selected, args):
472
        """Get curses data to display for a process.
473
474
        - p is the process to display
475
        - selected is a tag=True if p is the selected process
476
        """
477
        ret = [self.curse_new_line()]
478
479
        # When a process is selected:
480
        # * display a special character at the beginning of the line
481
        # * underline the command name
482
        ret.append(
483
            self.curse_add_line(
484
                unicode_message('PROCESS_SELECTOR') if (selected and not args.disable_cursor) else ' ', 'SELECTED'
485
            )
486
        )
487
488
        for stat in [i for i in self.enable_stats if i not in glances_processes.disable_stats]:
489
            msg = getattr(self, f'_get_process_curses_{stat}')(p, selected, args)
490
            if isinstance(msg, list):
491
                # ex: _get_process_curses_command return a list, so extend
492
                ret.extend(msg)
493
            else:
494
                # ex: _get_process_curses_cpu return a dict, so append
495
                ret.append(msg)
496
497
        return ret
498
499
    def is_selected_process(self, args):
500
        return (
501
            args.is_standalone
502
            and self.args.enable_process_extended
503
            and args.cursor_position is not None
504
            and glances_processes.extended_process is not None
505
        )
506
507
    def msg_curse(self, args=None, max_width=None):
508
        """Return the dict to display in the curse interface."""
509
        # Init the return message
510
        ret = []
511
512
        # Only process if stats exist and display plugin enable...
513
        if not self.stats or args.disable_process:
514
            return ret
515
516
        # Compute the sort key
517
        process_sort_key = glances_processes.sort_key
518
        processes_list_sorted = self._sort_stats(process_sort_key)
519
520
        # Display extended stats for selected process
521
        #############################################
522
523
        if self.is_selected_process(args):
524
            self._msg_curse_extended_process(ret, glances_processes.extended_process)
525
526
        # Display others processes list
527
        ###############################
528
529
        # Header
530
        self._msg_curse_header(ret, process_sort_key, args)
531
532
        # Process list
533
        # Loop over processes (sorted by the sort key previously compute)
534
        # This is a Glances bottleneck (see flame graph),
535
        # TODO: get_process_curses_data should be optimized
536
        for position, process in enumerate(processes_list_sorted):
537
            ret.extend(self.get_process_curses_data(process, position == args.cursor_position, args))
538
539
        # A filter is set Display the stats summaries
540
        if glances_processes.process_filter is not None:
541
            if args.reset_minmax_tag:
542
                args.reset_minmax_tag = not args.reset_minmax_tag
543
                self._mmm_reset()
544
            self._msg_curse_sum(ret, args=args)
545
            self._msg_curse_sum(ret, mmm='min', args=args)
546
            self._msg_curse_sum(ret, mmm='max', args=args)
547
548
        # Return the message with decoration
549
        return ret
550
551
    def _msg_curse_extended_process(self, ret, p):
552
        """Get extended curses data for the selected process (see issue #2225)
553
554
        The result depends of the process type (process or thread).
555
556
        Input p is a dict with the following keys:
557
        {'status': 'S',
558
         'memory_info': {'rss': 466890752, 'vms': 3365347328, 'shared': 68153344,
559
                         'text': 659456, 'lib': 0, 'data': 774647808, 'dirty': 0],
560
         'pid': 4980,
561
         'io_counters': [165385216, 0, 165385216, 0, 1],
562
         'num_threads': 20,
563
         'nice': 0,
564
         'memory_percent': 5.958135664449709,
565
         'cpu_percent': 0.0,
566
         'gids': {'real': 1000, 'effective': 1000, 'saved': 1000},
567
         'cpu_times': {'user': 696.38, 'system': 119.98, 'children_user': 0.0,
568
                       'children_system': 0.0, 'iowait': 0.0),
569
         'name': 'WebExtensions',
570
         'key': 'pid',
571
         'time_since_update': 2.1997854709625244,
572
         'cmdline': ['/snap/firefox/2154/usr/lib/firefox/firefox', '-contentproc', '-childID', '...'],
573
         'username': 'nicolargo',
574
         'cpu_min': 0.0,
575
         'cpu_max': 7.0,
576
         'cpu_mean': 3.2}
577
        """
578
        self._msg_curse_extended_process_thread(ret, p)
579
580
    def add_title_line(self, ret, prog):
581
        ret.append(self.curse_add_line("Pinned thread ", "TITLE"))
582
        ret.append(self.curse_add_line(prog['name'], "UNDERLINE"))
583
        ret.append(self.curse_add_line(" ('e' to unpin)"))
584
585
        return ret
586
587
    def add_cpu_line(self, ret, prog):
588
        ret.append(self.curse_new_line())
589
        ret.append(self.curse_add_line(' CPU Min/Max/Mean: '))
590
        msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(prog['cpu_min'], prog['cpu_max'], prog['cpu_mean'])
591
        ret.append(self.curse_add_line(msg, decoration='INFO'))
592
593
        return ret
594
595
    def maybe_add_cpu_affinity_line(self, ret, prog):
596
        if 'cpu_affinity' in prog and prog['cpu_affinity'] is not None:
597
            ret.append(self.curse_add_line(' Affinity: '))
598
            ret.append(self.curse_add_line(str(len(prog['cpu_affinity'])), decoration='INFO'))
599
            ret.append(self.curse_add_line(' cores', decoration='INFO'))
600
601
        return ret
602
603
    def add_ionice_line(self, headers, default):
604
        def add_ionice_using_matches(msg, v):
605
            return msg + headers.get(v, default(v))
606
607
        return add_ionice_using_matches
608
609
    def get_headers(self, k):
610
        # Linux: The scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle.
611
        default = {0: 'No specific I/O priority', 1: k + 'Real Time', 2: k + 'Best Effort', 3: k + 'IDLE'}
612
613
        # Windows: On Windows only ioclass is used and it can be set to 2 (normal), 1 (low) or 0 (very low).
614
        windows = {0: k + 'Very Low', 1: k + 'Low', 2: 'No specific I/O priority'}
615
616
        return windows if WINDOWS else default
617
618
    def maybe_add_ionice_line(self, ret, prog):
619
        if 'ionice' in prog and prog['ionice'] is not None and hasattr(prog['ionice'], 'ioclass'):
620
            msg = ' IO nice: '
621
            k = 'Class is '
622
            v = prog['ionice'].ioclass
623
624
            def default(v):
625
                return k + str(v)
0 ignored issues
show
introduced by
The variable k does not seem to be defined in case 'ionice' in prog and Sub...bscriptNode, 'ioclass') on line 619 is False. Are you sure this can never be the case?
Loading history...
626
627
            headers = self.get_headers(k)
628
            msg = self.add_ionice_line(headers, default)(msg, v)
629
            #  value is a number which goes from 0 to 7.
630
            # The higher the value, the lower the I/O priority of the process.
631
            if hasattr(prog['ionice'], 'value') and prog['ionice'].value != 0:
632
                msg += ' (value {}/7)'.format(str(prog['ionice'].value))
633
            ret.append(self.curse_add_line(msg, splittable=True))
634
635
        return ret
636
637
    def maybe_add_memory_swap_line(self, ret, prog):
638
        if 'memory_swap' in prog and prog['memory_swap'] is not None:
639
            ret.append(
640
                self.curse_add_line(
641
                    self.auto_unit(prog['memory_swap'], low_precision=False), decoration='INFO', splittable=True
642
                )
643
            )
644
            ret.append(self.curse_add_line(' swap ', splittable=True))
645
646
        return ret
647
648
    def add_memory_info_lines(self, ret, prog):
649
        for key, val in prog['memory_info'].items():
650
            ret.append(
651
                self.curse_add_line(
652
                    self.auto_unit(val, low_precision=False),
653
                    decoration='INFO',
654
                    splittable=True,
655
                )
656
            )
657
            ret.append(self.curse_add_line(' ' + key + ' ', splittable=True))
658
659
        return ret
660
661
    def add_memory_line(self, ret, prog):
662
        ret.append(self.curse_new_line())
663
        ret.append(self.curse_add_line(' MEM Min/Max/Mean: '))
664
        msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(prog['memory_min'], prog['memory_max'], prog['memory_mean'])
665
        ret.append(self.curse_add_line(msg, decoration='INFO'))
666
        if 'memory_info' in prog and prog['memory_info'] is not None:
667
            ret.append(self.curse_add_line(' Memory info: '))
668
            steps = [self.add_memory_info_lines, self.maybe_add_memory_swap_line]
669
            ret = functools.reduce(lambda ret, step: step(ret, prog), steps, ret)
670
671
        return ret
672
673
    def add_io_and_network_lines(self, ret, prog):
674
        ret.append(self.curse_new_line())
675
        ret.append(self.curse_add_line(' Open: '))
676
        for stat_prefix in ['num_threads', 'num_fds', 'num_handles', 'tcp', 'udp']:
677
            if stat_prefix in prog and prog[stat_prefix] is not None:
678
                ret.append(self.curse_add_line(str(prog[stat_prefix]), decoration='INFO'))
679
                ret.append(self.curse_add_line(' {} '.format(stat_prefix.replace('num_', ''))))
680
        return ret
681
682
    def _msg_curse_extended_process_thread(self, ret, prog):
683
        # `append_newlines` has dummy arguments for piping thru `functools.reduce`
684
        def append_newlines(ret, prog):
685
            (ret.append(self.curse_new_line()),)
686
            ret.append(self.curse_new_line())
687
688
            return ret
689
690
        steps = [
691
            self.add_title_line,
692
            self.add_cpu_line,
693
            self.maybe_add_cpu_affinity_line,
694
            self.maybe_add_ionice_line,
695
            self.add_memory_line,
696
            self.add_io_and_network_lines,
697
            append_newlines,
698
        ]
699
700
        functools.reduce(lambda ret, step: step(ret, prog), steps, ret)
701
702 View Code Duplication
    def _msg_curse_header(self, ret, process_sort_key, args=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
703
        """Build the header and add it to the ret dict."""
704
        sort_style = 'SORT'
705
706
        display_stats = [i for i in self.enable_stats if i not in glances_processes.disable_stats]
707
708
        if 'cpu_percent' in display_stats:
709
            if args.disable_irix and 0 < self.nb_log_core < 10:
710
                msg = self.layout_header['cpu'].format('CPU%/' + str(self.nb_log_core))
711
            elif args.disable_irix and self.nb_log_core != 0:
712
                msg = self.layout_header['cpu'].format('CPU%/C')
713
            else:
714
                msg = self.layout_header['cpu'].format('CPU%')
715
            ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_percent' else 'DEFAULT'))
716
717
        if 'memory_percent' in display_stats:
718
            msg = self.layout_header['mem'].format('MEM%')
719
            ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'memory_percent' else 'DEFAULT'))
720
        if 'memory_info' in display_stats:
721
            msg = self.layout_header['virt'].format('VIRT')
722
            ret.append(self.curse_add_line(msg, optional=True))
723
            msg = self.layout_header['res'].format('RES')
724
            ret.append(self.curse_add_line(msg, optional=True))
725
        if 'pid' in display_stats:
726
            msg = self.layout_header['pid'].format('PID', width=self._max_pid_size())
727
            ret.append(self.curse_add_line(msg))
728
        if 'username' in display_stats:
729
            msg = self.layout_header['user'].format('USER')
730
            ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'username' else 'DEFAULT'))
731
        if 'cpu_times' in display_stats:
732
            msg = self.layout_header['time'].format('TIME+')
733
            ret.append(
734
                self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_times' else 'DEFAULT', optional=True)
735
            )
736
        if 'num_threads' in display_stats:
737
            msg = self.layout_header['thread'].format('THR')
738
            ret.append(self.curse_add_line(msg))
739
        if 'nice' in display_stats:
740
            msg = self.layout_header['nice'].format('NI')
741
            ret.append(self.curse_add_line(msg))
742
        if 'status' in display_stats:
743
            msg = self.layout_header['status'].format('S')
744
            ret.append(self.curse_add_line(msg))
745
        if 'io_counters' in display_stats:
746
            msg = self.layout_header['ior'].format('R/s')
747
            ret.append(
748
                self.curse_add_line(
749
                    msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True
750
                )
751
            )
752
            msg = self.layout_header['iow'].format('W/s')
753
            ret.append(
754
                self.curse_add_line(
755
                    msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True
756
                )
757
            )
758
        if args.is_standalone and not args.disable_cursor:
759
            shortkey = "('e' to pin | 'k' to kill)"
760
        else:
761
            shortkey = ""
762
        if 'cmdline' in display_stats:
763
            msg = self.layout_header['command'].format("Command", shortkey)
764
            ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'name' else 'DEFAULT'))
765
766 View Code Duplication
    def _msg_curse_sum(self, ret, sep_char='_', mmm=None, args=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
767
        """
768
        Build the sum message (only when filter is on) and add it to the ret dict.
769
770
        :param ret: list of string where the message is added
771
        :param sep_char: define the line separation char
772
        :param mmm: display min, max, mean or current (if mmm=None)
773
        :param args: Glances args
774
        """
775
        ret.append(self.curse_new_line())
776
        if mmm is None:
777
            ret.append(self.curse_add_line(sep_char * 69))
778
            ret.append(self.curse_new_line())
779
        # CPU percent sum
780
        msg = ' '
781
        msg += self.layout_stat['cpu'].format(self._sum_stats('cpu_percent', mmm=mmm))
782
        ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm)))
783
        # MEM percent sum
784
        msg = self.layout_stat['mem'].format(self._sum_stats('memory_percent', mmm=mmm))
785
        ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm)))
786
        # VIRT and RES memory sum
787
        if (
788
            'memory_info' in self.stats[0]
789
            and self.stats[0]['memory_info'] is not None
790
            and self.stats[0]['memory_info'] != ''
791
        ):
792
            # VMS
793
            msg = self.layout_stat['virt'].format(
794
                self.auto_unit(self._sum_stats('memory_info', sub_key='vms', mmm=mmm), low_precision=False)
795
            )
796
            ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm), optional=True))
797
            # RSS
798
            msg = self.layout_stat['res'].format(
799
                self.auto_unit(self._sum_stats('memory_info', sub_key='rss', mmm=mmm), low_precision=False)
800
            )
801
            ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm), optional=True))
802
        else:
803
            msg = self.layout_header['virt'].format('')
804
            ret.append(self.curse_add_line(msg))
805
            msg = self.layout_header['res'].format('')
806
            ret.append(self.curse_add_line(msg))
807
        # PID
808
        msg = self.layout_header['pid'].format('', width=self._max_pid_size())
809
        ret.append(self.curse_add_line(msg))
810
        # USER
811
        msg = self.layout_header['user'].format('')
812
        ret.append(self.curse_add_line(msg))
813
        # TIME+
814
        msg = self.layout_header['time'].format('')
815
        ret.append(self.curse_add_line(msg, optional=True))
816
        # THREAD
817
        msg = self.layout_header['thread'].format('')
818
        ret.append(self.curse_add_line(msg))
819
        # NICE
820
        msg = self.layout_header['nice'].format('')
821
        ret.append(self.curse_add_line(msg))
822
        # STATUS
823
        msg = self.layout_header['status'].format('')
824
        ret.append(self.curse_add_line(msg))
825
        # IO read/write
826
        if 'io_counters' in self.stats[0] and mmm is None:
827
            # IO read
828
            io_rs = int(
829
                (self._sum_stats('io_counters', 0) - self._sum_stats('io_counters', sub_key=2, mmm=mmm))
830
                / self.stats[0]['time_since_update']
831
            )
832
            if io_rs == 0:
833
                msg = self.layout_stat['ior'].format('0')
834
            else:
835
                msg = self.layout_stat['ior'].format(self.auto_unit(io_rs, low_precision=True))
836
            ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm), optional=True, additional=True))
837
            # IO write
838
            io_ws = int(
839
                (self._sum_stats('io_counters', 1) - self._sum_stats('io_counters', sub_key=3, mmm=mmm))
840
                / self.stats[0]['time_since_update']
841
            )
842
            if io_ws == 0:
843
                msg = self.layout_stat['iow'].format('0')
844
            else:
845
                msg = self.layout_stat['iow'].format(self.auto_unit(io_ws, low_precision=True))
846
            ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm), optional=True, additional=True))
847
        else:
848
            msg = self.layout_header['ior'].format('')
849
            ret.append(self.curse_add_line(msg, optional=True, additional=True))
850
            msg = self.layout_header['iow'].format('')
851
            ret.append(self.curse_add_line(msg, optional=True, additional=True))
852
        if mmm is None:
853
            msg = '< {}'.format('current')
854
            ret.append(self.curse_add_line(msg, optional=True))
855
        else:
856
            msg = f'< {mmm}'
857
            ret.append(self.curse_add_line(msg, optional=True))
858
            msg = '(\'M\' to reset)'
859
            ret.append(self.curse_add_line(msg, optional=True))
860
861
    def _mmm_deco(self, mmm):
862
        """Return the decoration string for the current mmm status."""
863
        if mmm is not None:
864
            return 'DEFAULT'
865
        return 'FILTER'
866
867
    def _mmm_reset(self):
868
        """Reset the MMM stats."""
869
        self.mmm_min = {}
870
        self.mmm_max = {}
871
872
    def _sum_stats(self, key, sub_key=None, mmm=None):
873
        """Return the sum of the stats value for the given key.
874
875
        :param sub_key: If sub_key is set, get the p[key][sub_key]
876
        :param mmm: display min, max, mean or current (if mmm=None)
877
        """
878
        # Compute stats summary
879
        ret = 0
880
        for p in self.stats:
881
            if key not in p:
882
                # Correct issue #1188
883
                continue
884
            if p[key] is None:
885
                # Correct https://github.com/nicolargo/glances/issues/1105#issuecomment-363553788
886
                continue
887
            if sub_key is None:
888
                ret += p[key]
889
            else:
890
                ret += p[key][sub_key]
891
892
        # Manage Min/Max/Mean
893
        mmm_key = self._mmm_key(key, sub_key)
894
        if mmm == 'min':
895
            try:
896
                if self.mmm_min[mmm_key] > ret:
897
                    self.mmm_min[mmm_key] = ret
898
            except AttributeError:
899
                self.mmm_min = {}
900
                return 0
901
            except KeyError:
902
                self.mmm_min[mmm_key] = ret
903
            ret = self.mmm_min[mmm_key]
904
        elif mmm == 'max':
905
            try:
906
                if self.mmm_max[mmm_key] < ret:
907
                    self.mmm_max[mmm_key] = ret
908
            except AttributeError:
909
                self.mmm_max = {}
910
                return 0
911
            except KeyError:
912
                self.mmm_max[mmm_key] = ret
913
            ret = self.mmm_max[mmm_key]
914
915
        return ret
916
917
    def _mmm_key(self, key, sub_key):
918
        ret = key
919
        if sub_key is not None:
920
            ret += str(sub_key)
921
        return ret
922
923
    def _sort_stats(self, sorted_by=None):
924
        """Return the stats (dict) sorted by (sorted_by)."""
925
        return sort_stats(self.stats, sorted_by, reverse=glances_processes.sort_reverse)
926
927
    def _max_pid_size(self):
928
        """Return the maximum PID size in number of char."""
929
        if self.pid_max is not None:
930
            return len(str(self.pid_max))
931
932
        # By default return 5 (corresponding to 99999 PID number)
933
        return 5
934