Test Failed
Push — develop ( fa57d5...734632 )
by Nicolas
03:11 queued 40s
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
        try:
260
            if value in self.get_limit('nice_critical'):
261
                return 'CRITICAL'
262
        except KeyError:
263
            pass
264
        try:
265
            if value in self.get_limit('nice_warning'):
266
                return 'WARNING'
267
        except KeyError:
268
            pass
269
        try:
270
            if value in self.get_limit('nice_careful'):
271
                return 'CAREFUL'
272
        except KeyError:
273
            pass
274
        return 'DEFAULT'
275
276
    def _get_process_curses_cpu_percent(self, p, selected, args):
277
        """Return process CPU curses"""
278
        if key_exist_value_not_none_not_v('cpu_percent', p, ''):
279
            cpu_layout = self.layout_stat['cpu'] if p['cpu_percent'] < 100 else self.layout_stat['cpu_no_digit']
280
            if args.disable_irix and self.nb_log_core != 0:
281
                msg = cpu_layout.format(p['cpu_percent'] / float(self.nb_log_core))
282
            else:
283
                msg = cpu_layout.format(p['cpu_percent'])
284
            alert = self.get_alert(
285
                p['cpu_percent'],
286
                highlight_zero=False,
287
                is_max=(p['cpu_percent'] == self.max_values['cpu_percent']),
288
                header="cpu",
289
            )
290
            ret = self.curse_add_line(msg, alert)
291
        else:
292
            msg = self.layout_header['cpu'].format('?')
293
            ret = self.curse_add_line(msg)
294
        return ret
295
296
    def _get_process_curses_memory_percent(self, p, selected, args):
297
        """Return process MEM curses"""
298
        if key_exist_value_not_none_not_v('memory_percent', p, ''):
299
            msg = self.layout_stat['mem'].format(p['memory_percent'])
300
            alert = self.get_alert(
301
                p['memory_percent'],
302
                highlight_zero=False,
303
                is_max=(p['memory_percent'] == self.max_values['memory_percent']),
304
                header="mem",
305
            )
306
            ret = self.curse_add_line(msg, alert)
307
        else:
308
            msg = self.layout_header['mem'].format('?')
309
            ret = self.curse_add_line(msg)
310
        return ret
311
312
    def _get_process_curses_vms(self, p, selected, args):
313
        """Return process VMS curses"""
314
        if key_exist_value_not_none_not_v('memory_info', p, '', 1) and 'vms' in p['memory_info']:
315
            msg = self.layout_stat['virt'].format(self.auto_unit(p['memory_info']['vms'], low_precision=False))
316
            ret = self.curse_add_line(msg, optional=True)
317
        else:
318
            msg = self.layout_header['virt'].format('?')
319
            ret = self.curse_add_line(msg)
320
        return ret
321
322
    def _get_process_curses_rss(self, p, selected, args):
323
        """Return process RSS curses"""
324
        if key_exist_value_not_none_not_v('memory_info', p, '', 0) and 'rss' in p['memory_info']:
325
            msg = self.layout_stat['res'].format(self.auto_unit(p['memory_info']['rss'], low_precision=False))
326
            ret = self.curse_add_line(msg, optional=True)
327
        else:
328
            msg = self.layout_header['res'].format('?')
329
            ret = self.curse_add_line(msg)
330
        return ret
331
332
    def _get_process_curses_memory_info(self, p, selected, args):
333
        return [
334
            self._get_process_curses_vms(p, selected, args),
335
            self._get_process_curses_rss(p, selected, args),
336
        ]
337
338
    def _get_process_curses_pid(self, p, selected, args):
339
        """Return process PID curses"""
340
        # Display processes, so the PID should be displayed
341
        msg = self.layout_stat['pid'].format(p['pid'], width=self._max_pid_size())
342
        return self.curse_add_line(msg)
343
344
    def _get_process_curses_username(self, p, selected, args):
345
        """Return process username curses"""
346
        if 'username' in p:
347
            # docker internal users are displayed as ints only, therefore str()
348
            # Correct issue #886 on Windows OS
349
            msg = self.layout_stat['user'].format(str(p['username'])[:9])
350
        else:
351
            msg = self.layout_header['user'].format('?')
352
        return self.curse_add_line(msg)
353
354
    def _get_process_curses_cpu_times(self, p, selected, args):
355
        """Return process time curses"""
356
        cpu_times = p['cpu_times']
357
        try:
358
            # Sum user and system time
359
            user_system_time = cpu_times['user'] + cpu_times['system']
360
        except (OverflowError, TypeError, KeyError):
361
            # Catch OverflowError on some Amazon EC2 server
362
            # See https://github.com/nicolargo/glances/issues/87
363
            # Also catch TypeError on macOS
364
            # See: https://github.com/nicolargo/glances/issues/622
365
            # Also catch KeyError (as no stats be present for processes of other users)
366
            # See: https://github.com/nicolargo/glances/issues/2831
367
            # logger.debug("Cannot get TIME+ ({})".format(e))
368
            msg = self.layout_header['time'].format('?')
369
            return self.curse_add_line(msg, optional=True)
370
371
        hours, minutes, seconds = seconds_to_hms(user_system_time)
372
        if hours > 99:
373
            msg = f'{hours:<7}h'
374
        elif 0 < hours < 100:
375
            msg = f'{hours}h{minutes}:{seconds}'
376
        else:
377
            msg = f'{minutes}:{seconds}'
378
379
        msg = self.layout_stat['time'].format(msg)
380
        if hours > 0:
381
            return self.curse_add_line(msg, decoration='CPU_TIME', optional=True)
382
383
        return self.curse_add_line(msg, optional=True)
384
385
    def _get_process_curses_num_threads(self, p, selected, args):
386
        """Return process thread curses"""
387
        if 'num_threads' in p:
388
            num_threads = p['num_threads']
389
            if num_threads is None:
390
                num_threads = '?'
391
            msg = self.layout_stat['thread'].format(num_threads)
392
        else:
393
            msg = self.layout_header['thread'].format('?')
394
        return self.curse_add_line(msg)
395
396
    def _get_process_curses_nice(self, p, selected, args):
397
        """Return process nice curses"""
398
        if 'nice' in p:
399
            nice = p['nice']
400
            if nice is None:
401
                nice = '?'
402
            msg = self.layout_stat['nice'].format(nice)
403
            ret = self.curse_add_line(msg, decoration=self.get_nice_alert(nice))
404
        else:
405
            msg = self.layout_header['nice'].format('?')
406
            ret = self.curse_add_line(msg)
407
        return ret
408
409
    def _get_process_curses_status(self, p, selected, args):
410
        """Return process status curses"""
411
        if 'status' in p:
412
            status = p['status']
413
            msg = self.layout_stat['status'].format(status)
414
            if status == 'R':
415
                ret = self.curse_add_line(msg, decoration='STATUS')
416
            else:
417
                ret = self.curse_add_line(msg)
418
        else:
419
            msg = self.layout_header['status'].format('?')
420
            ret = self.curse_add_line(msg)
421
        return ret
422
423
    def _get_process_curses_io_read_write(self, p, selected, args, rorw='ior'):
424
        """Return process IO Read or Write curses"""
425
        if 'io_counters' in p and p['io_counters'][4] == 1 and p['time_since_update'] != 0:
426
            # Display rate if stats is available and io_tag ([4]) == 1
427
            # IO
428
            io = int(
429
                (p['io_counters'][0 if rorw == 'ior' else 1] - p['io_counters'][2 if rorw == 'ior' else 3])
430
                / p['time_since_update']
431
            )
432
            if io == 0:
433
                msg = self.layout_stat[rorw].format("0")
434
            else:
435
                msg = self.layout_stat[rorw].format(self.auto_unit(io, low_precision=True))
436
            ret = self.curse_add_line(msg, optional=True, additional=True)
437
        else:
438
            msg = self.layout_header[rorw].format("?")
439
            ret = self.curse_add_line(msg, optional=True, additional=True)
440
        return ret
441
442
    def _get_process_curses_io_counters(self, p, selected, args):
443
        return [
444
            self._get_process_curses_io_read_write(p, selected, args, rorw='ior'),
445
            self._get_process_curses_io_read_write(p, selected, args, rorw='iow'),
446
        ]
447
448
    def _get_process_curses_cmdline(self, p, selected, args):
449
        """Return process cmdline curses"""
450
        ret = []
451
        # If no command line for the process is available, fallback to the bare process name instead
452
        bare_process_name = p['name']
453
        cmdline = p.get('cmdline', '?')
454
        try:
455
            process_decoration = 'PROCESS_SELECTED' if (selected and not args.disable_cursor) else 'PROCESS'
456
            if cmdline:
457
                path, cmd, arguments = split_cmdline(bare_process_name, cmdline)
458
                # Manage end of line in arguments (see #1692)
459
                arguments = replace_special_chars(arguments)
460
                if os.path.isdir(path) and not args.process_short_name:
461
                    msg = self.layout_stat['command'].format(path) + os.sep
462
                    ret.append(self.curse_add_line(msg, splittable=True))
463
                    ret.append(self.curse_add_line(cmd, decoration=process_decoration, splittable=True))
464
                else:
465
                    msg = self.layout_stat['command'].format(cmd)
466
                    ret.append(self.curse_add_line(msg, decoration=process_decoration, splittable=True))
467
                if arguments:
468
                    msg = ' ' + self.layout_stat['command'].format(arguments)
469
                    ret.append(self.curse_add_line(msg, splittable=True))
470
            else:
471
                msg = self.layout_stat['name'].format(bare_process_name)
472
                ret.append(self.curse_add_line(msg, decoration=process_decoration, splittable=True))
473
        except (TypeError, UnicodeEncodeError) as e:
474
            # Avoid crash after running fine for several hours #1335
475
            logger.debug(f"Can not decode command line '{cmdline}' ({e})")
476
            ret.append(self.curse_add_line('', splittable=True))
477
        return ret
478
479
    def get_process_curses_data(self, p, selected, args):
480
        """Get curses data to display for a process.
481
482
        - p is the process to display
483
        - selected is a tag=True if p is the selected process
484
        """
485
        ret = [self.curse_new_line()]
486
487
        # When a process is selected:
488
        # * display a special character at the beginning of the line
489
        # * underline the command name
490
        ret.append(
491
            self.curse_add_line(
492
                unicode_message('PROCESS_SELECTOR') if (selected and not args.disable_cursor) else ' ', 'SELECTED'
493
            )
494
        )
495
496
        for stat in [i for i in self.enable_stats if i not in glances_processes.disable_stats]:
497
            msg = getattr(self, f'_get_process_curses_{stat}')(p, selected, args)
498
            if isinstance(msg, list):
499
                # ex: _get_process_curses_command return a list, so extend
500
                ret.extend(msg)
501
            else:
502
                # ex: _get_process_curses_cpu return a dict, so append
503
                ret.append(msg)
504
505
        return ret
506
507
    def is_selected_process(self, args):
508
        return (
509
            args.is_standalone
510
            and self.args.enable_process_extended
511
            and args.cursor_position is not None
512
            and glances_processes.extended_process is not None
513
        )
514
515
    def msg_curse(self, args=None, max_width=None):
516
        """Return the dict to display in the curse interface."""
517
        # Init the return message
518
        ret = []
519
520
        # Only process if stats exist and display plugin enable...
521
        if not self.stats or args.disable_process:
522
            return ret
523
524
        # Compute the sort key
525
        process_sort_key = glances_processes.sort_key
526
        processes_list_sorted = self._sort_stats(process_sort_key)
527
528
        # Display extended stats for selected process
529
        #############################################
530
531
        if self.is_selected_process(args):
532
            self._msg_curse_extended_process(ret, glances_processes.extended_process)
533
534
        # Display others processes list
535
        ###############################
536
537
        # Header
538
        self._msg_curse_header(ret, process_sort_key, args)
539
540
        # Process list
541
        # Loop over processes (sorted by the sort key previously compute)
542
        # This is a Glances bottleneck (see flame graph),
543
        # TODO: get_process_curses_data should be optimized
544
        for position, process in enumerate(processes_list_sorted):
545
            ret.extend(self.get_process_curses_data(process, position == args.cursor_position, args))
546
547
        # A filter is set Display the stats summaries
548
        if glances_processes.process_filter is not None:
549
            if args.reset_minmax_tag:
550
                args.reset_minmax_tag = not args.reset_minmax_tag
551
                self._mmm_reset()
552
            self._msg_curse_sum(ret, args=args)
553
            self._msg_curse_sum(ret, mmm='min', args=args)
554
            self._msg_curse_sum(ret, mmm='max', args=args)
555
556
        # Return the message with decoration
557
        return ret
558
559
    def _msg_curse_extended_process(self, ret, p):
560
        """Get extended curses data for the selected process (see issue #2225)
561
562
        The result depends of the process type (process or thread).
563
564
        Input p is a dict with the following keys:
565
        {'status': 'S',
566
         'memory_info': {'rss': 466890752, 'vms': 3365347328, 'shared': 68153344,
567
                         'text': 659456, 'lib': 0, 'data': 774647808, 'dirty': 0],
568
         'pid': 4980,
569
         'io_counters': [165385216, 0, 165385216, 0, 1],
570
         'num_threads': 20,
571
         'nice': 0,
572
         'memory_percent': 5.958135664449709,
573
         'cpu_percent': 0.0,
574
         'gids': {'real': 1000, 'effective': 1000, 'saved': 1000},
575
         'cpu_times': {'user': 696.38, 'system': 119.98, 'children_user': 0.0,
576
                       'children_system': 0.0, 'iowait': 0.0),
577
         'name': 'WebExtensions',
578
         'key': 'pid',
579
         'time_since_update': 2.1997854709625244,
580
         'cmdline': ['/snap/firefox/2154/usr/lib/firefox/firefox', '-contentproc', '-childID', '...'],
581
         'username': 'nicolargo',
582
         'cpu_min': 0.0,
583
         'cpu_max': 7.0,
584
         'cpu_mean': 3.2}
585
        """
586
        self._msg_curse_extended_process_thread(ret, p)
587
588
    def add_title_line(self, ret, prog):
589
        ret.append(self.curse_add_line("Pinned thread ", "TITLE"))
590
        ret.append(self.curse_add_line(prog['name'], "UNDERLINE"))
591
        ret.append(self.curse_add_line(" ('e' to unpin)"))
592
593
        return ret
594
595
    def add_cpu_line(self, ret, prog):
596
        ret.append(self.curse_new_line())
597
        ret.append(self.curse_add_line(' CPU Min/Max/Mean: '))
598
        msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(prog['cpu_min'], prog['cpu_max'], prog['cpu_mean'])
599
        ret.append(self.curse_add_line(msg, decoration='INFO'))
600
601
        return ret
602
603
    def maybe_add_cpu_affinity_line(self, ret, prog):
604
        if 'cpu_affinity' in prog and prog['cpu_affinity'] is not None:
605
            ret.append(self.curse_add_line(' Affinity: '))
606
            ret.append(self.curse_add_line(str(len(prog['cpu_affinity'])), decoration='INFO'))
607
            ret.append(self.curse_add_line(' cores', decoration='INFO'))
608
609
        return ret
610
611
    def add_ionice_line(self, headers, default):
612
        def add_ionice_using_matches(msg, v):
613
            return msg + headers.get(v, default(v))
614
615
        return add_ionice_using_matches
616
617
    def get_headers(self, k):
618
        # Linux: The scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle.
619
        default = {0: 'No specific I/O priority', 1: k + 'Real Time', 2: k + 'Best Effort', 3: k + 'IDLE'}
620
621
        # Windows: On Windows only ioclass is used and it can be set to 2 (normal), 1 (low) or 0 (very low).
622
        windows = {0: k + 'Very Low', 1: k + 'Low', 2: 'No specific I/O priority'}
623
624
        return windows if WINDOWS else default
625
626
    def maybe_add_ionice_line(self, ret, prog):
627
        if 'ionice' in prog and prog['ionice'] is not None and hasattr(prog['ionice'], 'ioclass'):
628
            msg = ' IO nice: '
629
            k = 'Class is '
630
            v = prog['ionice'].ioclass
631
632
            def default(v):
633
                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 627 is False. Are you sure this can never be the case?
Loading history...
634
635
            headers = self.get_headers(k)
636
            msg = self.add_ionice_line(headers, default)(msg, v)
637
            #  value is a number which goes from 0 to 7.
638
            # The higher the value, the lower the I/O priority of the process.
639
            if hasattr(prog['ionice'], 'value') and prog['ionice'].value != 0:
640
                msg += ' (value {}/7)'.format(str(prog['ionice'].value))
641
            ret.append(self.curse_add_line(msg, splittable=True))
642
643
        return ret
644
645
    def maybe_add_memory_swap_line(self, ret, prog):
646
        if 'memory_swap' in prog and prog['memory_swap'] is not None:
647
            ret.append(
648
                self.curse_add_line(
649
                    self.auto_unit(prog['memory_swap'], low_precision=False), decoration='INFO', splittable=True
650
                )
651
            )
652
            ret.append(self.curse_add_line(' swap ', splittable=True))
653
654
        return ret
655
656
    def add_memory_info_lines(self, ret, prog):
657
        for key, val in prog['memory_info'].items():
658
            ret.append(
659
                self.curse_add_line(
660
                    self.auto_unit(val, low_precision=False),
661
                    decoration='INFO',
662
                    splittable=True,
663
                )
664
            )
665
            ret.append(self.curse_add_line(' ' + key + ' ', splittable=True))
666
667
        return ret
668
669
    def add_memory_line(self, ret, prog):
670
        ret.append(self.curse_new_line())
671
        ret.append(self.curse_add_line(' MEM Min/Max/Mean: '))
672
        msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(prog['memory_min'], prog['memory_max'], prog['memory_mean'])
673
        ret.append(self.curse_add_line(msg, decoration='INFO'))
674
        if 'memory_info' in prog and prog['memory_info'] is not None:
675
            ret.append(self.curse_add_line(' Memory info: '))
676
            steps = [self.add_memory_info_lines, self.maybe_add_memory_swap_line]
677
            ret = functools.reduce(lambda ret, step: step(ret, prog), steps, ret)
678
679
        return ret
680
681
    def add_io_and_network_lines(self, ret, prog):
682
        ret.append(self.curse_new_line())
683
        ret.append(self.curse_add_line(' Open: '))
684
        for stat_prefix in ['num_threads', 'num_fds', 'num_handles', 'tcp', 'udp']:
685
            if stat_prefix in prog and prog[stat_prefix] is not None:
686
                ret.append(self.curse_add_line(str(prog[stat_prefix]), decoration='INFO'))
687
                ret.append(self.curse_add_line(' {} '.format(stat_prefix.replace('num_', ''))))
688
        return ret
689
690
    def _msg_curse_extended_process_thread(self, ret, prog):
691
        # `append_newlines` has dummy arguments for piping thru `functools.reduce`
692
        def append_newlines(ret, prog):
693
            (ret.append(self.curse_new_line()),)
694
            ret.append(self.curse_new_line())
695
696
            return ret
697
698
        steps = [
699
            self.add_title_line,
700
            self.add_cpu_line,
701
            self.maybe_add_cpu_affinity_line,
702
            self.maybe_add_ionice_line,
703
            self.add_memory_line,
704
            self.add_io_and_network_lines,
705
            append_newlines,
706
        ]
707
708
        functools.reduce(lambda ret, step: step(ret, prog), steps, ret)
709
710 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...
711
        """Build the header and add it to the ret dict."""
712
        sort_style = 'SORT'
713
714
        display_stats = [i for i in self.enable_stats if i not in glances_processes.disable_stats]
715
716
        if 'cpu_percent' in display_stats:
717
            if args.disable_irix and 0 < self.nb_log_core < 10:
718
                msg = self.layout_header['cpu'].format('CPU%/' + str(self.nb_log_core))
719
            elif args.disable_irix and self.nb_log_core != 0:
720
                msg = self.layout_header['cpu'].format('CPU%/C')
721
            else:
722
                msg = self.layout_header['cpu'].format('CPU%')
723
            ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_percent' else 'DEFAULT'))
724
725
        if 'memory_percent' in display_stats:
726
            msg = self.layout_header['mem'].format('MEM%')
727
            ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'memory_percent' else 'DEFAULT'))
728
        if 'memory_info' in display_stats:
729
            msg = self.layout_header['virt'].format('VIRT')
730
            ret.append(self.curse_add_line(msg, optional=True))
731
            msg = self.layout_header['res'].format('RES')
732
            ret.append(self.curse_add_line(msg, optional=True))
733
        if 'pid' in display_stats:
734
            msg = self.layout_header['pid'].format('PID', width=self._max_pid_size())
735
            ret.append(self.curse_add_line(msg))
736
        if 'username' in display_stats:
737
            msg = self.layout_header['user'].format('USER')
738
            ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'username' else 'DEFAULT'))
739
        if 'cpu_times' in display_stats:
740
            msg = self.layout_header['time'].format('TIME+')
741
            ret.append(
742
                self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_times' else 'DEFAULT', optional=True)
743
            )
744
        if 'num_threads' in display_stats:
745
            msg = self.layout_header['thread'].format('THR')
746
            ret.append(self.curse_add_line(msg))
747
        if 'nice' in display_stats:
748
            msg = self.layout_header['nice'].format('NI')
749
            ret.append(self.curse_add_line(msg))
750
        if 'status' in display_stats:
751
            msg = self.layout_header['status'].format('S')
752
            ret.append(self.curse_add_line(msg))
753
        if 'io_counters' in display_stats:
754
            msg = self.layout_header['ior'].format('R/s')
755
            ret.append(
756
                self.curse_add_line(
757
                    msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True
758
                )
759
            )
760
            msg = self.layout_header['iow'].format('W/s')
761
            ret.append(
762
                self.curse_add_line(
763
                    msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True
764
                )
765
            )
766
        if args.is_standalone and not args.disable_cursor:
767
            shortkey = "('e' to pin | 'k' to kill)"
768
        else:
769
            shortkey = ""
770
        if 'cmdline' in display_stats:
771
            msg = self.layout_header['command'].format("Command", shortkey)
772
            ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'name' else 'DEFAULT'))
773
774 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...
775
        """
776
        Build the sum message (only when filter is on) and add it to the ret dict.
777
778
        :param ret: list of string where the message is added
779
        :param sep_char: define the line separation char
780
        :param mmm: display min, max, mean or current (if mmm=None)
781
        :param args: Glances args
782
        """
783
        ret.append(self.curse_new_line())
784
        if mmm is None:
785
            ret.append(self.curse_add_line(sep_char * 69))
786
            ret.append(self.curse_new_line())
787
        # CPU percent sum
788
        msg = ' '
789
        msg += self.layout_stat['cpu'].format(self._sum_stats('cpu_percent', mmm=mmm))
790
        ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm)))
791
        # MEM percent sum
792
        msg = self.layout_stat['mem'].format(self._sum_stats('memory_percent', mmm=mmm))
793
        ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm)))
794
        # VIRT and RES memory sum
795
        if (
796
            'memory_info' in self.stats[0]
797
            and self.stats[0]['memory_info'] is not None
798
            and self.stats[0]['memory_info'] != ''
799
        ):
800
            # VMS
801
            msg = self.layout_stat['virt'].format(
802
                self.auto_unit(self._sum_stats('memory_info', sub_key='vms', mmm=mmm), low_precision=False)
803
            )
804
            ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm), optional=True))
805
            # RSS
806
            msg = self.layout_stat['res'].format(
807
                self.auto_unit(self._sum_stats('memory_info', sub_key='rss', mmm=mmm), low_precision=False)
808
            )
809
            ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm), optional=True))
810
        else:
811
            msg = self.layout_header['virt'].format('')
812
            ret.append(self.curse_add_line(msg))
813
            msg = self.layout_header['res'].format('')
814
            ret.append(self.curse_add_line(msg))
815
        # PID
816
        msg = self.layout_header['pid'].format('', width=self._max_pid_size())
817
        ret.append(self.curse_add_line(msg))
818
        # USER
819
        msg = self.layout_header['user'].format('')
820
        ret.append(self.curse_add_line(msg))
821
        # TIME+
822
        msg = self.layout_header['time'].format('')
823
        ret.append(self.curse_add_line(msg, optional=True))
824
        # THREAD
825
        msg = self.layout_header['thread'].format('')
826
        ret.append(self.curse_add_line(msg))
827
        # NICE
828
        msg = self.layout_header['nice'].format('')
829
        ret.append(self.curse_add_line(msg))
830
        # STATUS
831
        msg = self.layout_header['status'].format('')
832
        ret.append(self.curse_add_line(msg))
833
        # IO read/write
834
        if 'io_counters' in self.stats[0] and mmm is None:
835
            # IO read
836
            io_rs = int(
837
                (self._sum_stats('io_counters', 0) - self._sum_stats('io_counters', sub_key=2, mmm=mmm))
838
                / self.stats[0]['time_since_update']
839
            )
840
            if io_rs == 0:
841
                msg = self.layout_stat['ior'].format('0')
842
            else:
843
                msg = self.layout_stat['ior'].format(self.auto_unit(io_rs, low_precision=True))
844
            ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm), optional=True, additional=True))
845
            # IO write
846
            io_ws = int(
847
                (self._sum_stats('io_counters', 1) - self._sum_stats('io_counters', sub_key=3, mmm=mmm))
848
                / self.stats[0]['time_since_update']
849
            )
850
            if io_ws == 0:
851
                msg = self.layout_stat['iow'].format('0')
852
            else:
853
                msg = self.layout_stat['iow'].format(self.auto_unit(io_ws, low_precision=True))
854
            ret.append(self.curse_add_line(msg, decoration=self._mmm_deco(mmm), optional=True, additional=True))
855
        else:
856
            msg = self.layout_header['ior'].format('')
857
            ret.append(self.curse_add_line(msg, optional=True, additional=True))
858
            msg = self.layout_header['iow'].format('')
859
            ret.append(self.curse_add_line(msg, optional=True, additional=True))
860
        if mmm is None:
861
            msg = '< {}'.format('current')
862
            ret.append(self.curse_add_line(msg, optional=True))
863
        else:
864
            msg = f'< {mmm}'
865
            ret.append(self.curse_add_line(msg, optional=True))
866
            msg = '(\'M\' to reset)'
867
            ret.append(self.curse_add_line(msg, optional=True))
868
869
    def _mmm_deco(self, mmm):
870
        """Return the decoration string for the current mmm status."""
871
        if mmm is not None:
872
            return 'DEFAULT'
873
        return 'FILTER'
874
875
    def _mmm_reset(self):
876
        """Reset the MMM stats."""
877
        self.mmm_min = {}
878
        self.mmm_max = {}
879
880
    def _sum_stats(self, key, sub_key=None, mmm=None):
881
        """Return the sum of the stats value for the given key.
882
883
        :param sub_key: If sub_key is set, get the p[key][sub_key]
884
        :param mmm: display min, max, mean or current (if mmm=None)
885
        """
886
        # Compute stats summary
887
        ret = 0
888
        for p in self.stats:
889
            if key not in p:
890
                # Correct issue #1188
891
                continue
892
            if p[key] is None:
893
                # Correct https://github.com/nicolargo/glances/issues/1105#issuecomment-363553788
894
                continue
895
            if sub_key is None:
896
                ret += p[key]
897
            else:
898
                ret += p[key][sub_key]
899
900
        # Manage Min/Max/Mean
901
        mmm_key = self._mmm_key(key, sub_key)
902
        if mmm == 'min':
903
            try:
904
                if self.mmm_min[mmm_key] > ret:
905
                    self.mmm_min[mmm_key] = ret
906
            except AttributeError:
907
                self.mmm_min = {}
908
                return 0
909
            except KeyError:
910
                self.mmm_min[mmm_key] = ret
911
            ret = self.mmm_min[mmm_key]
912
        elif mmm == 'max':
913
            try:
914
                if self.mmm_max[mmm_key] < ret:
915
                    self.mmm_max[mmm_key] = ret
916
            except AttributeError:
917
                self.mmm_max = {}
918
                return 0
919
            except KeyError:
920
                self.mmm_max[mmm_key] = ret
921
            ret = self.mmm_max[mmm_key]
922
923
        return ret
924
925
    def _mmm_key(self, key, sub_key):
926
        ret = key
927
        if sub_key is not None:
928
            ret += str(sub_key)
929
        return ret
930
931
    def _sort_stats(self, sorted_by=None):
932
        """Return the stats (dict) sorted by (sorted_by)."""
933
        return sort_stats(self.stats, sorted_by, reverse=glances_processes.sort_reverse)
934
935
    def _max_pid_size(self):
936
        """Return the maximum PID size in number of char."""
937
        if self.pid_max is not None:
938
            return len(str(self.pid_max))
939
940
        # By default return 5 (corresponding to 99999 PID number)
941
        return 5
942