ProcesslistPlugin.add_title_line()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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