Test Failed
Pull Request — develop (#3035)
by
unknown
02:25
created

glances.plugins.processlist.PluginModel.__init__()   B

Complexity

Conditions 5

Size

Total Lines 38
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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