ProcesslistPlugin.get_export()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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