Test Failed
Pull Request — develop (#2915)
by
unknown
02:16
created

PluginModel.get_headers()   A

Complexity

Conditions 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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