Test Failed
Push — develop ( 435b25...d99d36 )
by Nicolas
03:26
created

ProcesslistPlugin.get_key()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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