PluginModel.__msg_curse_header()   F
last analyzed

Complexity

Conditions 17

Size

Total Lines 55
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 17
eloc 44
nop 4
dl 0
loc 55
rs 1.8
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like glances.plugins.processlist.PluginModel.__msg_curse_header() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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