ProcesslistPlugin.__init__()   A
last analyzed

Complexity

Conditions 4

Size

Total Lines 29
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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