Test Failed
Push — develop ( f98060...e1cd3d )
by Nicolas
02:32
created

PluginModel.add_cpu_line()   A

Complexity

Conditions 1

Size

Total Lines 7
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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