Test Failed
Push — master ( ee826a...d9056e )
by Nicolas
03:09
created

glances.plugins.processlist   F

Complexity

Total Complexity 157

Size/Duplication

Total Lines 908
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 556
dl 0
loc 908
rs 2
c 0
b 0
f 0
wmc 157

2 Functions

Rating   Name   Duplication   Size   Complexity  
A seconds_to_hms() 0 10 1
A split_cmdline() 0 16 2

32 Methods

Rating   Name   Duplication   Size   Complexity  
A PluginModel.is_selected_process() 0 6 1
A PluginModel.update() 0 16 2
A PluginModel.__mmm_key() 0 5 2
B PluginModel._get_process_curses_io() 0 18 7
B PluginModel.msg_curse() 0 43 7
F PluginModel.__msg_curse_extended_process_thread() 0 81 24
A PluginModel._get_process_curses_status() 0 13 3
B PluginModel.get_nice_alert() 0 19 7
C PluginModel.get_process_curses_data() 0 86 10
C PluginModel.__init__() 0 42 9
A PluginModel._get_process_curses_cpu() 0 19 5
A PluginModel._get_process_curses_vms() 0 9 3
A PluginModel._get_process_curses_rss() 0 9 3
A PluginModel.__msg_curse_extended_process() 0 31 2
A PluginModel.__mmm_deco() 0 6 2
B PluginModel._get_process_curses_time() 0 27 6
A PluginModel._get_process_curses_io_read() 0 3 1
A PluginModel._get_process_curses_io_write() 0 3 1
A PluginModel._get_process_curses_mem() 0 15 2
A PluginModel._get_process_curses_thread() 0 12 3
A PluginModel.__sort_stats() 0 3 1
F PluginModel.__msg_curse_header() 0 55 17
A PluginModel.update_local() 0 10 2
A PluginModel._get_process_curses_nice() 0 12 3
A PluginModel.__max_pid_size() 0 7 2
A PluginModel.get_key() 0 3 1
A PluginModel._get_process_curses_username() 0 11 2
D PluginModel.__sum_stats() 0 44 13
A PluginModel.__msg_curse_extended_process_program() 0 7 1
C PluginModel.__msg_curse_sum() 0 93 10
A PluginModel.get_export() 0 6 1
A PluginModel.__mmm_reset() 0 4 1

How to fix   Complexity   

Complexity

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