Test Failed
Push — develop ( e1cd3d...105435 )
by Nicolas
03:12 queued 27s
created

glances.plugins.processlist.PluginModel.load()   B

Complexity

Conditions 7

Size

Total Lines 20
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 16
nop 3
dl 0
loc 20
rs 8
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
    # Default list of processes stats to be grabbed / displayed
121
    # Can be altered by glances_processes.disable_stats
122
    enable_stats = [
123
        'cpu_percent',
124
        'memory_percent',
125
        'memory_info',  # vms and rss
126
        'pid',
127
        'username',
128
        'cpu_times',
129
        'num_threads',
130
        'nice',
131
        'status',
132
        'io_counters',  # ior and iow
133
        'cmdline',
134
    ]
135
136
    # Define the header layout of the processes list columns
137
    layout_header = {
138
        'cpu': '{:<6} ',
139
        'mem': '{:<5} ',
140
        'virt': '{:<5} ',
141
        'res': '{:<5} ',
142
        'pid': '{:>{width}} ',
143
        'user': '{:<10} ',
144
        'time': '{:>8} ',
145
        'thread': '{:<3} ',
146
        'nice': '{:>3} ',
147
        'status': '{:>1} ',
148
        'ior': '{:>4} ',
149
        'iow': '{:<4} ',
150
        'command': '{} {}',
151
    }
152
153
    # Define the stat layout of the processes list columns
154
    layout_stat = {
155
        'cpu': '{:<6.1f}',
156
        'cpu_no_digit': '{:<6.0f}',
157
        'mem': '{:<5.1f} ',
158
        'virt': '{:<5} ',
159
        'res': '{:<5} ',
160
        'pid': '{:>{width}} ',
161
        'user': '{:<10} ',
162
        'time': '{:>8} ',
163
        'thread': '{:<3} ',
164
        'nice': '{:>3} ',
165
        'status': '{:>1} ',
166
        'ior': '{:>4} ',
167
        'iow': '{:<4} ',
168
        'command': '{}',
169
        'name': '[{}]',
170
    }
171
172
    def __init__(self, args=None, config=None):
173
        """Init the plugin."""
174
        super().__init__(args=args, config=config, fields_description=fields_description, stats_init_value=[])
175
176
        # We want to display the stat in the curse interface
177
        self.display_curse = True
178
179
        # Trying to display proc time
180
        self.tag_proc_time = True
181
182
        # Call CorePluginModel to get the core number (needed when not in IRIX mode / Solaris mode)
183
        try:
184
            self.nb_log_core = CorePluginModel(args=self.args).update()["log"]
185
        except Exception:
186
            self.nb_log_core = 0
187
188
        # Get the max values (dict)
189
        self.max_values = copy.deepcopy(glances_processes.max_values())
190
191
        # Get the maximum PID number
192
        # Use to optimize space (see https://github.com/nicolargo/glances/issues/959)
193
        self.pid_max = glances_processes.pid_max
194
195
        # Load the config file
196
        self.load(args, config)
197
198
        # The default sort key could also be overwrite by command line (see #1903)
199
        if args and args.sort_processes_key is not None:
200
            glances_processes.set_sort_key(args.sort_processes_key, False)
201
202
        # Note: 'glances_processes' is already init in the processes.py script
203
204
    def load(self, args, config):
205
        # Set the default sort key if it is defined in the configuration file
206
        if config is None or 'processlist' not in config.as_dict():
207
            return
208
        if 'sort_key' in config.as_dict()['processlist']:
209
            logger.debug(
210
                'Configuration overwrites processes sort key by {}'.format(config.as_dict()['processlist']['sort_key'])
211
            )
212
            glances_processes.set_sort_key(config.as_dict()['processlist']['sort_key'], False)
213
        if 'export' in config.as_dict()['processlist']:
214
            glances_processes.export_process_filter = config.as_dict()['processlist']['export']
215
            if args.export:
216
                logger.info("Export process filter is set to: {}".format(config.as_dict()['processlist']['export']))
217
        if 'disable_stats' in config.as_dict()['processlist']:
218
            logger.info(
219
                'Followings processes stats wil not be displayed: {}'.format(
220
                    config.as_dict()['processlist']['disable_stats']
221
                )
222
            )
223
            glances_processes.disable_stats = config.as_dict()['processlist']['disable_stats'].split(',')
224
225
    def get_key(self):
226
        """Return the key of the list."""
227
        return 'pid'
228
229
    def update(self):
230
        """Update processes stats using the input method."""
231
        # Update the stats
232
        if self.input_method == 'local':
233
            stats = self.update_local()
234
        else:
235
            stats = self.get_init_value()
236
237
        # Get the max values (dict)
238
        # Use Deep copy to avoid change between update and display
239
        self.max_values = copy.deepcopy(glances_processes.max_values())
240
241
        # Update the stats
242
        self.stats = stats
243
244
        return self.stats
245
246
    def update_local(self):
247
        # Update stats using the standard system lib
248
        # Note: Update is done in the processcount plugin
249
        # Just return the result
250
        if self.args.programs:
251
            stats = glances_processes.get_list(as_programs=True)
252
        else:
253
            stats = glances_processes.get_list()
254
255
        return stats
256
257
    def get_export(self):
258
        """Return the processes list to export.
259
        Not all the processeses are exported.
260
        Only the one defined in the Glances configuration file (see #794 for details).
261
        """
262
        return glances_processes.get_export()
263
264
    def get_nice_alert(self, value):
265
        """Return the alert relative to the Nice configuration list"""
266
        value = str(value)
267
        try:
268
            if value in self.get_limit('nice_critical'):
269
                return 'CRITICAL'
270
        except KeyError:
271
            pass
272
        try:
273
            if value in self.get_limit('nice_warning'):
274
                return 'WARNING'
275
        except KeyError:
276
            pass
277
        try:
278
            if value in self.get_limit('nice_careful'):
279
                return 'CAREFUL'
280
        except KeyError:
281
            pass
282
        return 'DEFAULT'
283
284
    def _get_process_curses_cpu_percent(self, p, selected, args):
285
        """Return process CPU curses"""
286
        if key_exist_value_not_none_not_v('cpu_percent', p, ''):
287
            cpu_layout = self.layout_stat['cpu'] if p['cpu_percent'] < 100 else self.layout_stat['cpu_no_digit']
288
            if args.disable_irix and self.nb_log_core != 0:
289
                msg = cpu_layout.format(p['cpu_percent'] / float(self.nb_log_core))
290
            else:
291
                msg = cpu_layout.format(p['cpu_percent'])
292
            alert = self.get_alert(
293
                p['cpu_percent'],
294
                highlight_zero=False,
295
                is_max=(p['cpu_percent'] == self.max_values['cpu_percent']),
296
                header="cpu",
297
            )
298
            ret = self.curse_add_line(msg, alert)
299
        else:
300
            msg = self.layout_header['cpu'].format('?')
301
            ret = self.curse_add_line(msg)
302
        return ret
303
304
    def _get_process_curses_memory_percent(self, p, selected, args):
305
        """Return process MEM curses"""
306
        if key_exist_value_not_none_not_v('memory_percent', p, ''):
307
            msg = self.layout_stat['mem'].format(p['memory_percent'])
308
            alert = self.get_alert(
309
                p['memory_percent'],
310
                highlight_zero=False,
311
                is_max=(p['memory_percent'] == self.max_values['memory_percent']),
312
                header="mem",
313
            )
314
            ret = self.curse_add_line(msg, alert)
315
        else:
316
            msg = self.layout_header['mem'].format('?')
317
            ret = self.curse_add_line(msg)
318
        return ret
319
320
    def _get_process_curses_vms(self, p, selected, args):
321
        """Return process VMS curses"""
322
        if key_exist_value_not_none_not_v('memory_info', p, '', 1) and 'vms' in p['memory_info']:
323
            msg = self.layout_stat['virt'].format(self.auto_unit(p['memory_info']['vms'], low_precision=False))
324
            ret = self.curse_add_line(msg, optional=True)
325
        else:
326
            msg = self.layout_header['virt'].format('?')
327
            ret = self.curse_add_line(msg)
328
        return ret
329
330
    def _get_process_curses_rss(self, p, selected, args):
331
        """Return process RSS curses"""
332
        if key_exist_value_not_none_not_v('memory_info', p, '', 0) and 'rss' in p['memory_info']:
333
            msg = self.layout_stat['res'].format(self.auto_unit(p['memory_info']['rss'], low_precision=False))
334
            ret = self.curse_add_line(msg, optional=True)
335
        else:
336
            msg = self.layout_header['res'].format('?')
337
            ret = self.curse_add_line(msg)
338
        return ret
339
340
    def _get_process_curses_memory_info(self, p, selected, args):
341
        return [
342
            self._get_process_curses_vms(p, selected, args),
343
            self._get_process_curses_rss(p, selected, args),
344
        ]
345
346
    def _get_process_curses_pid(self, p, selected, args):
347
        """Return process PID curses"""
348
        if not self.args.programs:
349
            # Display processes, so the PID should be displayed
350
            msg = self.layout_stat['pid'].format(p['pid'], width=self.__max_pid_size())
351
        else:
352
            # Display programs, so the PID should not be displayed
353
            # Instead displays the number of children
354
            msg = self.layout_stat['pid'].format(
355
                len(p['childrens']) if 'childrens' in p else '_', width=self.__max_pid_size()
356
            )
357
        return self.curse_add_line(msg)
358
359
    def _get_process_curses_username(self, p, selected, args):
360
        """Return process username curses"""
361
        if 'username' in p:
362
            # docker internal users are displayed as ints only, therefore str()
363
            # Correct issue #886 on Windows OS
364
            msg = self.layout_stat['user'].format(str(p['username'])[:9])
365
        else:
366
            msg = self.layout_header['user'].format('?')
367
        return self.curse_add_line(msg)
368
369
    def _get_process_curses_cpu_times(self, p, selected, args):
370
        """Return process time curses"""
371
        cpu_times = p['cpu_times']
372
        try:
373
            # Sum user and system time
374
            user_system_time = cpu_times['user'] + cpu_times['system']
375
        except (OverflowError, TypeError, KeyError):
376
            # Catch OverflowError on some Amazon EC2 server
377
            # See https://github.com/nicolargo/glances/issues/87
378
            # Also catch TypeError on macOS
379
            # See: https://github.com/nicolargo/glances/issues/622
380
            # Also catch KeyError (as no stats be present for processes of other users)
381
            # See: https://github.com/nicolargo/glances/issues/2831
382
            # logger.debug("Cannot get TIME+ ({})".format(e))
383
            msg = self.layout_header['time'].format('?')
384
            return self.curse_add_line(msg, optional=True)
385
386
        hours, minutes, seconds = seconds_to_hms(user_system_time)
387
        if hours > 99:
388
            msg = f'{hours:<7}h'
389
        elif 0 < hours < 100:
390
            msg = f'{hours}h{minutes}:{seconds}'
391
        else:
392
            msg = f'{minutes}:{seconds}'
393
394
        msg = self.layout_stat['time'].format(msg)
395
        if hours > 0:
396
            return self.curse_add_line(msg, decoration='CPU_TIME', optional=True)
397
398
        return self.curse_add_line(msg, optional=True)
399
400
    def _get_process_curses_num_threads(self, p, selected, args):
401
        """Return process thread curses"""
402
        if 'num_threads' in p:
403
            num_threads = p['num_threads']
404
            if num_threads is None:
405
                num_threads = '?'
406
            msg = self.layout_stat['thread'].format(num_threads)
407
        else:
408
            msg = self.layout_header['thread'].format('?')
409
        return self.curse_add_line(msg)
410
411
    def _get_process_curses_nice(self, p, selected, args):
412
        """Return process nice curses"""
413
        if 'nice' in p:
414
            nice = p['nice']
415
            if nice is None:
416
                nice = '?'
417
            msg = self.layout_stat['nice'].format(nice)
418
            ret = self.curse_add_line(msg, decoration=self.get_nice_alert(nice))
419
        else:
420
            msg = self.layout_header['nice'].format('?')
421
            ret = self.curse_add_line(msg)
422
        return ret
423
424
    def _get_process_curses_status(self, p, selected, args):
425
        """Return process status curses"""
426
        if 'status' in p:
427
            status = p['status']
428
            msg = self.layout_stat['status'].format(status)
429
            if status == 'R':
430
                ret = self.curse_add_line(msg, decoration='STATUS')
431
            else:
432
                ret = self.curse_add_line(msg)
433
        else:
434
            msg = self.layout_header['status'].format('?')
435
            ret = self.curse_add_line(msg)
436
        return ret
437
438
    def _get_process_curses_io_read_write(self, p, selected, args, rorw='ior'):
439
        """Return process IO Read or Write curses"""
440
        if 'io_counters' in p and p['io_counters'][4] == 1 and p['time_since_update'] != 0:
441
            # Display rate if stats is available and io_tag ([4]) == 1
442
            # IO
443
            io = int(
444
                (p['io_counters'][0 if rorw == 'ior' else 1] - p['io_counters'][2 if rorw == 'ior' else 3])
445
                / p['time_since_update']
446
            )
447
            if io == 0:
448
                msg = self.layout_stat[rorw].format("0")
449
            else:
450
                msg = self.layout_stat[rorw].format(self.auto_unit(io, low_precision=True))
451
            ret = self.curse_add_line(msg, optional=True, additional=True)
452
        else:
453
            msg = self.layout_header[rorw].format("?")
454
            ret = self.curse_add_line(msg, optional=True, additional=True)
455
        return ret
456
457
    def _get_process_curses_io_counters(self, p, selected, args):
458
        return [
459
            self._get_process_curses_io_read_write(p, selected, args, rorw='ior'),
460
            self._get_process_curses_io_read_write(p, selected, args, rorw='iow'),
461
        ]
462
463
    def _get_process_curses_cmdline(self, p, selected, args):
464
        """Return process cmdline curses"""
465
        ret = []
466
        # If no command line for the process is available, fallback to the bare process name instead
467
        bare_process_name = p['name']
468
        cmdline = p.get('cmdline', '?')
469
        try:
470
            process_decoration = 'PROCESS_SELECTED' if (selected and not args.disable_cursor) else 'PROCESS'
471
            if cmdline:
472
                path, cmd, arguments = split_cmdline(bare_process_name, cmdline)
473
                # Manage end of line in arguments (see #1692)
474
                arguments = replace_special_chars(arguments)
475
                if os.path.isdir(path) and not args.process_short_name:
476
                    msg = self.layout_stat['command'].format(path) + os.sep
477
                    ret.append(self.curse_add_line(msg, splittable=True))
478
                    ret.append(self.curse_add_line(cmd, decoration=process_decoration, splittable=True))
479
                else:
480
                    msg = self.layout_stat['command'].format(cmd)
481
                    ret.append(self.curse_add_line(msg, decoration=process_decoration, splittable=True))
482
                if arguments:
483
                    msg = ' ' + self.layout_stat['command'].format(arguments)
484
                    ret.append(self.curse_add_line(msg, splittable=True))
485
            else:
486
                msg = self.layout_stat['name'].format(bare_process_name)
487
                ret.append(self.curse_add_line(msg, decoration=process_decoration, splittable=True))
488
        except (TypeError, UnicodeEncodeError) as e:
489
            # Avoid crash after running fine for several hours #1335
490
            logger.debug(f"Can not decode command line '{cmdline}' ({e})")
491
            ret.append(self.curse_add_line('', splittable=True))
492
        return ret
493
494
    def get_process_curses_data(self, p, selected, args):
495
        """Get curses data to display for a process.
496
497
        - p is the process to display
498
        - selected is a tag=True if p is the selected process
499
        """
500
        ret = [self.curse_new_line()]
501
502
        # When a process is selected:
503
        # * display a special character at the beginning of the line
504
        # * underline the command name
505
        ret.append(
506
            self.curse_add_line(
507
                unicode_message('PROCESS_SELECTOR') if (selected and not args.disable_cursor) else ' ', 'SELECTED'
508
            )
509
        )
510
511
        for stat in [i for i in self.enable_stats if i not in glances_processes.disable_stats]:
512
            msg = getattr(self, f'_get_process_curses_{stat}')(p, selected, args)
513
            if isinstance(msg, list):
514
                # ex: _get_process_curses_command return a list, so extend
515
                ret.extend(msg)
516
            else:
517
                # ex: _get_process_curses_cpu return a dict, so append
518
                ret.append(msg)
519
520
        return ret
521
522
    def is_selected_process(self, args):
523
        return (
524
            args.is_standalone
525
            and self.args.enable_process_extended
526
            and args.cursor_position is not None
527
            and glances_processes.extended_process is not None
528
        )
529
530
    def msg_curse(self, args=None, max_width=None):
531
        """Return the dict to display in the curse interface."""
532
        # Init the return message
533
        ret = []
534
535
        # Only process if stats exist and display plugin enable...
536
        if not self.stats or args.disable_process:
537
            return ret
538
539
        # Compute the sort key
540
        process_sort_key = glances_processes.sort_key
541
        processes_list_sorted = self.__sort_stats(process_sort_key)
542
543
        # Display extended stats for selected process
544
        #############################################
545
546
        if self.is_selected_process(args):
547
            self.__msg_curse_extended_process(ret, glances_processes.extended_process)
548
549
        # Display others processes list
550
        ###############################
551
552
        # Header
553
        self.__msg_curse_header(ret, process_sort_key, args)
554
555
        # Process list
556
        # Loop over processes (sorted by the sort key previously compute)
557
        # This is a Glances bottleneck (see flame graph),
558
        # TODO: get_process_curses_data should be optimized
559
        for position, process in enumerate(processes_list_sorted):
560
            ret.extend(self.get_process_curses_data(process, position == args.cursor_position, args))
561
562
        # A filter is set Display the stats summaries
563
        if glances_processes.process_filter is not None:
564
            if args.reset_minmax_tag:
565
                args.reset_minmax_tag = not args.reset_minmax_tag
566
                self.__mmm_reset()
567
            self.__msg_curse_sum(ret, args=args)
568
            self.__msg_curse_sum(ret, mmm='min', args=args)
569
            self.__msg_curse_sum(ret, mmm='max', args=args)
570
571
        # Return the message with decoration
572
        return ret
573
574
    def __msg_curse_extended_process(self, ret, p):
575
        """Get extended curses data for the selected process (see issue #2225)
576
577
        The result depends of the process type (process or thread).
578
579
        Input p is a dict with the following keys:
580
        {'status': 'S',
581
         'memory_info': {'rss': 466890752, 'vms': 3365347328, 'shared': 68153344,
582
                         'text': 659456, 'lib': 0, 'data': 774647808, 'dirty': 0],
583
         'pid': 4980,
584
         'io_counters': [165385216, 0, 165385216, 0, 1],
585
         'num_threads': 20,
586
         'nice': 0,
587
         'memory_percent': 5.958135664449709,
588
         'cpu_percent': 0.0,
589
         'gids': {'real': 1000, 'effective': 1000, 'saved': 1000},
590
         'cpu_times': {'user': 696.38, 'system': 119.98, 'children_user': 0.0,
591
                       'children_system': 0.0, 'iowait': 0.0),
592
         'name': 'WebExtensions',
593
         'key': 'pid',
594
         'time_since_update': 2.1997854709625244,
595
         'cmdline': ['/snap/firefox/2154/usr/lib/firefox/firefox', '-contentproc', '-childID', '...'],
596
         'username': 'nicolargo',
597
         'cpu_min': 0.0,
598
         'cpu_max': 7.0,
599
         'cpu_mean': 3.2}
600
        """
601
        if self.args.programs:
602
            self.__msg_curse_extended_process_program(ret, p)
603
        else:
604
            self.__msg_curse_extended_process_thread(ret, p)
605
606
    def __msg_curse_extended_process_program(self, ret, p):
607
        # Title
608
        msg = "Pinned program {} ('e' to unpin)".format(p['name'])
609
        ret.append(self.curse_add_line(msg, "TITLE"))
610
611
        ret.append(self.curse_new_line())
612
        ret.append(self.curse_new_line())
613
614
    def add_title_line(self, ret, prog):
615
        ret.append(self.curse_add_line("Pinned thread ", "TITLE"))
616
        ret.append(self.curse_add_line(prog['name'], "UNDERLINE"))
617
        ret.append(self.curse_add_line(" ('e' to unpin)"))
618
619
        return ret
620
621
    def add_cpu_line(self, ret, prog):
622
        ret.append(self.curse_new_line())
623
        ret.append(self.curse_add_line(' CPU Min/Max/Mean: '))
624
        msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(prog['cpu_min'], prog['cpu_max'], prog['cpu_mean'])
625
        ret.append(self.curse_add_line(msg, decoration='INFO'))
626
627
        return ret
628
629
    def maybe_add_cpu_affinity_line(self, ret, prog):
630
        if 'cpu_affinity' in prog and prog['cpu_affinity'] is not None:
631
            ret.append(self.curse_add_line(' Affinity: '))
632
            ret.append(self.curse_add_line(str(len(prog['cpu_affinity'])), decoration='INFO'))
633
            ret.append(self.curse_add_line(' cores', decoration='INFO'))
634
635
        return ret
636
637
    def add_ionice_line(self, headers, default):
638
        def add_ionice_using_matches(msg, v):
639
            return msg + headers.get(v, default(v))
640
641
        return add_ionice_using_matches
642
643
    def get_headers(self, k):
644
        # Linux: The scheduling class. 0 for none, 1 for real time, 2 for best-effort, 3 for idle.
645
        default = {0: 'No specific I/O priority', 1: k + 'Real Time', 2: k + 'Best Effort', 3: k + 'IDLE'}
646
647
        # Windows: On Windows only ioclass is used and it can be set to 2 (normal), 1 (low) or 0 (very low).
648
        windows = {0: k + 'Very Low', 1: k + 'Low', 2: 'No specific I/O priority'}
649
650
        return windows if WINDOWS else default
651
652
    def maybe_add_ionice_line(self, ret, prog):
653
        if 'ionice' in prog and prog['ionice'] is not None and hasattr(prog['ionice'], 'ioclass'):
654
            msg = ' IO nice: '
655
            k = 'Class is '
656
            v = prog['ionice'].ioclass
657
658
            def default(v):
659
                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 653 is False. Are you sure this can never be the case?
Loading history...
660
661
            headers = self.get_headers(k)
662
            msg = self.add_ionice_line(headers, default)(msg, v)
663
            #  value is a number which goes from 0 to 7.
664
            # The higher the value, the lower the I/O priority of the process.
665
            if hasattr(prog['ionice'], 'value') and prog['ionice'].value != 0:
666
                msg += ' (value {}/7)'.format(str(prog['ionice'].value))
667
            ret.append(self.curse_add_line(msg, splittable=True))
668
669
        return ret
670
671
    def maybe_add_memory_swap_line(self, ret, prog):
672
        if 'memory_swap' in prog and prog['memory_swap'] is not None:
673
            ret.append(
674
                self.curse_add_line(
675
                    self.auto_unit(prog['memory_swap'], low_precision=False), decoration='INFO', splittable=True
676
                )
677
            )
678
            ret.append(self.curse_add_line(' swap ', splittable=True))
679
680
        return ret
681
682
    def add_memory_info_lines(self, ret, prog):
683
        for key, val in prog['memory_info'].items():
684
            ret.append(
685
                self.curse_add_line(
686
                    self.auto_unit(val, low_precision=False),
687
                    decoration='INFO',
688
                    splittable=True,
689
                )
690
            )
691
            ret.append(self.curse_add_line(' ' + key + ' ', splittable=True))
692
693
        return ret
694
695
    def add_memory_line(self, ret, prog):
696
        ret.append(self.curse_new_line())
697
        ret.append(self.curse_add_line(' MEM Min/Max/Mean: '))
698
        msg = '{: >7.1f}{: >7.1f}{: >7.1f}%'.format(prog['memory_min'], prog['memory_max'], prog['memory_mean'])
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[stat_prefix]), 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
        display_stats = [i for i in self.enable_stats if i not in glances_processes.disable_stats]
741
742
        if 'cpu_percent' in display_stats:
743
            if args.disable_irix and 0 < self.nb_log_core < 10:
744
                msg = self.layout_header['cpu'].format('CPU%/' + str(self.nb_log_core))
745
            elif args.disable_irix and self.nb_log_core != 0:
746
                msg = self.layout_header['cpu'].format('CPU%/C')
747
            else:
748
                msg = self.layout_header['cpu'].format('CPU%')
749
            ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_percent' else 'DEFAULT'))
750
751
        if 'memory_percent' in display_stats:
752
            msg = self.layout_header['mem'].format('MEM%')
753
            ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'memory_percent' else 'DEFAULT'))
754
        if 'memory_info' in display_stats:
755
            msg = self.layout_header['virt'].format('VIRT')
756
            ret.append(self.curse_add_line(msg, optional=True))
757
            msg = self.layout_header['res'].format('RES')
758
            ret.append(self.curse_add_line(msg, optional=True))
759
        if 'pid' in display_stats:
760
            if not self.args.programs:
761
                msg = self.layout_header['pid'].format('PID', width=self.__max_pid_size())
762
            else:
763
                msg = self.layout_header['pid'].format('NPROCS', width=self.__max_pid_size())
764
            ret.append(self.curse_add_line(msg))
765
        if 'username' in display_stats:
766
            msg = self.layout_header['user'].format('USER')
767
            ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'username' else 'DEFAULT'))
768
        if 'cpu_times' in display_stats:
769
            msg = self.layout_header['time'].format('TIME+')
770
            ret.append(
771
                self.curse_add_line(msg, sort_style if process_sort_key == 'cpu_times' else 'DEFAULT', optional=True)
772
            )
773
        if 'num_threads' in display_stats:
774
            msg = self.layout_header['thread'].format('THR')
775
            ret.append(self.curse_add_line(msg))
776
        if 'nice' in display_stats:
777
            msg = self.layout_header['nice'].format('NI')
778
            ret.append(self.curse_add_line(msg))
779
        if 'status' in display_stats:
780
            msg = self.layout_header['status'].format('S')
781
            ret.append(self.curse_add_line(msg))
782
        if 'io_counters' in display_stats:
783
            msg = self.layout_header['ior'].format('R/s')
784
            ret.append(
785
                self.curse_add_line(
786
                    msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True
787
                )
788
            )
789
            msg = self.layout_header['iow'].format('W/s')
790
            ret.append(
791
                self.curse_add_line(
792
                    msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True
793
                )
794
            )
795
        if args.is_standalone and not args.disable_cursor:
796
            if self.args.programs:
797
                shortkey = "('k' to kill)"
798
            else:
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("Programs" if self.args.programs else "Command", shortkey)
804
            ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'name' else 'DEFAULT'))
805
806
    def __msg_curse_sum(self, ret, sep_char='_', mmm=None, args=None):
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
            else:
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 __sort_stats(self, sorted_by=None):
964
        """Return the stats (dict) sorted by (sorted_by)."""
965
        return sort_stats(self.stats, sorted_by, reverse=glances_processes.sort_reverse)
966
967
    def __max_pid_size(self):
968
        """Return the maximum PID size in number of char."""
969
        if self.pid_max is not None:
970
            return len(str(self.pid_max))
971
972
        # By default return 5 (corresponding to 99999 PID number)
973
        return 5
974