Test Failed
Push — master ( 05aaee...10b5c2 )
by Nicolas
04:12 queued 14s
created

Plugin.__mmm_key()   A

Complexity

Conditions 2

Size

Total Lines 5
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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