ProcesslistPlugin._get_process_curses_cpu_times()   A
last analyzed

Complexity

Conditions 5

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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