Test Failed
Pull Request — develop (#3035)
by
unknown
03:55
created

PluginModel.add_cpu_line()   A

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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