Test Failed
Push — master ( d0fde6...8e443d )
by Nicolas
03:51 queued 15s
created

glances.processes   F

Complexity

Total Complexity 86

Size/Duplication

Total Lines 536
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 272
dl 0
loc 536
rs 2
c 0
b 0
f 0
wmc 86

28 Methods

Rating   Name   Duplication   Size   Complexity  
A GlancesProcesses.get_count() 0 3 1
A GlancesProcesses.processes_count() 0 4 1
A GlancesProcesses.process_filter_input() 0 4 1
A GlancesProcesses.process_filter() 0 4 1
A GlancesProcesses.max_processes() 0 4 1
A GlancesProcesses.max_values() 0 3 1
A GlancesProcesses.set_sort_key() 0 8 2
A GlancesProcesses.set_max_values() 0 3 1
A GlancesProcesses.enable_extended() 0 4 1
F GlancesProcesses.update() 0 170 25
A GlancesProcesses.disable() 0 3 1
A GlancesProcesses.process_filter_re() 0 4 1
A GlancesProcesses.sort_key() 0 4 1
A GlancesProcesses.reset_processcount() 0 3 1
B GlancesProcesses.__init__() 0 73 5
A GlancesProcesses.get_max_values() 0 3 1
A GlancesProcesses.disable_extended() 0 3 1
A GlancesProcesses.process_filter_key() 0 4 1
A GlancesProcesses.sort_reverse() 0 7 3
A GlancesProcesses.disable_kernel_threads() 0 3 1
A GlancesProcesses.update_processcount() 0 12 3
A GlancesProcesses.reset_max_values() 0 5 2
A GlancesProcesses.enable() 0 4 1
A GlancesProcesses.getlist() 0 3 1
A GlancesProcesses.pid_max() 0 32 4
A GlancesProcesses.kill() 0 7 1
A GlancesProcesses.nice_decrease() 0 10 2
A GlancesProcesses.nice_increase() 0 10 2

5 Functions

Rating   Name   Duplication   Size   Complexity  
A _sort_lambda() 0 8 3
A weighted() 0 3 2
C sort_stats() 0 34 10
A _sort_cpu_times() 0 9 1
A _sort_io_counters() 0 6 1

How to fix   Complexity   

Complexity

Complex classes like glances.processes often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of Glances.
4
#
5
# SPDX-FileCopyrightText: 2022 Nicolas Hennion <[email protected]>
6
#
7
# SPDX-License-Identifier: LGPL-3.0-only
8
#
9
10
import os
11
12
from glances.compat import iterkeys
13
from glances.globals import BSD, LINUX, MACOS, WINDOWS
14
from glances.timer import Timer, getTimeSinceLastUpdate
15
from glances.filter import GlancesFilter
16
from glances.logger import logger
17
18
import psutil
19
20
# This constant defines the list of available processes sort key
21
sort_processes_key_list = ['cpu_percent', 'memory_percent', 'username', 'cpu_times', 'io_counters', 'name']
22
23
24
class GlancesProcesses(object):
25
    """Get processed stats using the psutil library."""
26
27
    def __init__(self, cache_timeout=60):
28
        """Init the class to collect stats about processes."""
29
        # Add internals caches because psutil do not cache all the stats
30
        # See: https://github.com/giampaolo/psutil/issues/462
31
        self.username_cache = {}
32
        self.cmdline_cache = {}
33
34
        # The internals caches will be cleaned each 'cache_timeout' seconds
35
        self.cache_timeout = cache_timeout
36
        # First iteration, no cache
37
        self.cache_timer = Timer(0)
38
39
        # Init the io_old dict used to compute the IO bitrate
40
        # key = pid
41
        # value = [ read_bytes_old, write_bytes_old ]
42
        self.io_old = {}
43
44
        # Init stats
45
        self.auto_sort = None
46
        self._sort_key = None
47
        # Default processes sort key is 'auto'
48
        # Can be overwrite from the configuration file (issue#1536) => See glances_processlist.py init
49
        self.set_sort_key('auto', auto=True)
50
        self.processlist = []
51
        self.reset_processcount()
52
53
        # Cache is a dict with key=pid and value = dict of cached value
54
        self.processlist_cache = {}
55
56
        # Tag to enable/disable the processes stats (to reduce the Glances CPU consumption)
57
        # Default is to enable the processes stats
58
        self.disable_tag = False
59
60
        # Extended stats for top process is enable by default
61
        self.disable_extended_tag = False
62
63
        # Test if the system can grab io_counters
64
        try:
65
            p = psutil.Process()
66
            p.io_counters()
67
        except Exception as e:
68
            logger.warning('PsUtil can not grab processes io_counters ({})'.format(e))
69
            self.disable_io_counters = True
70
        else:
71
            logger.debug('PsUtil can grab processes io_counters')
72
            self.disable_io_counters = False
73
74
        # Test if the system can grab gids
75
        try:
76
            p = psutil.Process()
77
            p.gids()
78
        except Exception as e:
79
            logger.warning('PsUtil can not grab processes gids ({})'.format(e))
80
            self.disable_gids = True
81
        else:
82
            logger.debug('PsUtil can grab processes gids')
83
            self.disable_gids = False
84
85
        # Maximum number of processes showed in the UI (None if no limit)
86
        self._max_processes = None
87
88
        # Process filter is a regular expression
89
        self._filter = GlancesFilter()
90
91
        # Whether or not to hide kernel threads
92
        self.no_kernel_threads = False
93
94
        # Store maximums values in a dict
95
        # Used in the UI to highlight the maximum value
96
        self._max_values_list = ('cpu_percent', 'memory_percent')
97
        # { 'cpu_percent': 0.0, 'memory_percent': 0.0 }
98
        self._max_values = {}
99
        self.reset_max_values()
100
101
    def reset_processcount(self):
102
        """Reset the global process count"""
103
        self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0, 'pid_max': None}
104
105
    def update_processcount(self, plist):
106
        """Update the global process count from the current processes list"""
107
        # Update the maximum process ID (pid) number
108
        self.processcount['pid_max'] = self.pid_max
109
        # For each key in the processcount dict
110
        # count the number of processes with the same status
111
        for k in iterkeys(self.processcount):
112
            self.processcount[k] = len(list(filter(lambda v: v['status'] is k, plist)))
0 ignored issues
show
introduced by
The variable k does not seem to be defined in case the for loop on line 111 is not entered. Are you sure this can never be the case?
Loading history...
113
        # Compute thread
114
        self.processcount['thread'] = sum(i['num_threads'] for i in plist if i['num_threads'] is not None)
115
        # Compute total
116
        self.processcount['total'] = len(plist)
117
118
    def enable(self):
119
        """Enable process stats."""
120
        self.disable_tag = False
121
        self.update()
122
123
    def disable(self):
124
        """Disable process stats."""
125
        self.disable_tag = True
126
127
    def enable_extended(self):
128
        """Enable extended process stats."""
129
        self.disable_extended_tag = False
130
        self.update()
131
132
    def disable_extended(self):
133
        """Disable extended process stats."""
134
        self.disable_extended_tag = True
135
136
    @property
137
    def pid_max(self):
138
        """
139
        Get the maximum PID value.
140
141
        On Linux, the value is read from the `/proc/sys/kernel/pid_max` file.
142
143
        From `man 5 proc`:
144
        The default value for this file, 32768, results in the same range of
145
        PIDs as on earlier kernels. On 32-bit platforms, 32768 is the maximum
146
        value for pid_max. On 64-bit systems, pid_max can be set to any value
147
        up to 2^22 (PID_MAX_LIMIT, approximately 4 million).
148
149
        If the file is unreadable or not available for whatever reason,
150
        returns None.
151
152
        Some other OSes:
153
        - On FreeBSD and macOS the maximum is 99999.
154
        - On OpenBSD >= 6.0 the maximum is 99999 (was 32766).
155
        - On NetBSD the maximum is 30000.
156
157
        :returns: int or None
158
        """
159
        if LINUX:
160
            # XXX: waiting for https://github.com/giampaolo/psutil/issues/720
161
            try:
162
                with open('/proc/sys/kernel/pid_max', 'rb') as f:
163
                    return int(f.read())
164
            except (OSError, IOError):
165
                return None
166
        else:
167
            return None
168
169
    @property
170
    def processes_count(self):
171
        """Get the current number of processes showed in the UI."""
172
        return min(self._max_processes - 2, glances_processes.processcount['total'] - 1)
173
174
    @property
175
    def max_processes(self):
176
        """Get the maximum number of processes showed in the UI."""
177
        return self._max_processes
178
179
    @max_processes.setter
180
    def max_processes(self, value):
181
        """Set the maximum number of processes showed in the UI."""
182
        self._max_processes = value
183
184
    @property
185
    def process_filter_input(self):
186
        """Get the process filter (given by the user)."""
187
        return self._filter.filter_input
188
189
    @property
190
    def process_filter(self):
191
        """Get the process filter (current apply filter)."""
192
        return self._filter.filter
193
194
    @process_filter.setter
195
    def process_filter(self, value):
196
        """Set the process filter."""
197
        self._filter.filter = value
198
199
    @property
200
    def process_filter_key(self):
201
        """Get the process filter key."""
202
        return self._filter.filter_key
203
204
    @property
205
    def process_filter_re(self):
206
        """Get the process regular expression compiled."""
207
        return self._filter.filter_re
208
209
    def disable_kernel_threads(self):
210
        """Ignore kernel threads in process list."""
211
        self.no_kernel_threads = True
212
213
    @property
214
    def sort_reverse(self):
215
        """Return True to sort processes in reverse 'key' order, False instead."""
216
        if self.sort_key == 'name' or self.sort_key == 'username':
217
            return False
218
219
        return True
220
221
    def max_values(self):
222
        """Return the max values dict."""
223
        return self._max_values
224
225
    def get_max_values(self, key):
226
        """Get the maximum values of the given stat (key)."""
227
        return self._max_values[key]
228
229
    def set_max_values(self, key, value):
230
        """Set the maximum value for a specific stat (key)."""
231
        self._max_values[key] = value
232
233
    def reset_max_values(self):
234
        """Reset the maximum values dict."""
235
        self._max_values = {}
236
        for k in self._max_values_list:
237
            self._max_values[k] = 0.0
238
239
    def update(self):
240
        """Update the processes stats."""
241
        # Reset the stats
242
        self.processlist = []
243
        self.reset_processcount()
244
245
        # Do not process if disable tag is set
246
        if self.disable_tag:
247
            return
248
249
        # Time since last update (for disk_io rate computation)
250
        time_since_update = getTimeSinceLastUpdate('process_disk')
251
252
        # Grab standard stats
253
        #####################
254
        sorted_attrs = ['cpu_percent', 'cpu_times', 'memory_percent', 'name', 'status', 'num_threads']
255
        displayed_attr = ['memory_info', 'nice', 'pid']
256
        cached_attrs = ['cmdline', 'username']
257
258
        # Some stats are optional
259
        if not self.disable_io_counters:
260
            sorted_attrs.append('io_counters')
261
        if not self.disable_gids:
262
            displayed_attr.append('gids')
263
        # Some stats are not sort key
264
        # An optimisation can be done be only grabbed displayed_attr
265
        # for displayed processes (but only in standalone mode...)
266
        sorted_attrs.extend(displayed_attr)
267
        # Some stats are cached (not necessary to be refreshed every time)
268
        if self.cache_timer.finished():
269
            sorted_attrs += cached_attrs
270
            self.cache_timer.set(self.cache_timeout)
271
            self.cache_timer.reset()
272
            is_cached = False
273
        else:
274
            is_cached = True
275
276
        # Build the processes stats list (it is why we need psutil>=5.3.0)
277
        self.processlist = [
278
            p.info
279
            for p in psutil.process_iter(attrs=sorted_attrs, ad_value=None)
280
            # OS-related processes filter
281
            if not (BSD and p.info['name'] == 'idle')
282
            and not (WINDOWS and p.info['name'] == 'System Idle Process')
283
            and not (MACOS and p.info['name'] == 'kernel_task')
284
            and
285
            # Kernel threads filter
286
            not (self.no_kernel_threads and LINUX and p.info['gids'].real == 0)
287
        ]
288
289
        # Sort the processes list by the current sort_key
290
        self.processlist = sort_stats(self.processlist, sorted_by=self.sort_key, reverse=True)
291
292
        # Update the processcount
293
        self.update_processcount(self.processlist)
294
295
        # Loop over processes and add metadata
296
        first = True
297
        for proc in self.processlist:
298
            # Get extended stats, only for top processes (see issue #403).
299
            if first and not self.disable_extended_tag:
300
                # - cpu_affinity (Linux, Windows, FreeBSD)
301
                # - ionice (Linux and Windows > Vista)
302
                # - num_ctx_switches (not available on Illumos/Solaris)
303
                # - num_fds (Unix-like)
304
                # - num_handles (Windows)
305
                # - memory_maps (only swap, Linux)
306
                #   https://www.cyberciti.biz/faq/linux-which-process-is-using-swap/
307
                # - connections (TCP and UDP)
308
                extended = {}
309
                try:
310
                    top_process = psutil.Process(proc['pid'])
311
                    extended_stats = ['cpu_affinity', 'ionice', 'num_ctx_switches']
312
                    if LINUX:
313
                        # num_fds only avalable on Unix system (see issue #1351)
314
                        extended_stats += ['num_fds']
315
                    if WINDOWS:
316
                        extended_stats += ['num_handles']
317
318
                    # Get the extended stats
319
                    extended = top_process.as_dict(attrs=extended_stats, ad_value=None)
320
321
                    if LINUX:
322
                        try:
323
                            extended['memory_swap'] = sum([v.swap for v in top_process.memory_maps()])
324
                        except (psutil.NoSuchProcess, KeyError):
325
                            # (KeyError catch for issue #1551)
326
                            pass
327
                        except (psutil.AccessDenied, NotImplementedError):
328
                            # NotImplementedError: /proc/${PID}/smaps file doesn't exist
329
                            # on kernel < 2.6.14 or CONFIG_MMU kernel configuration option
330
                            # is not enabled (see psutil #533/glances #413).
331
                            extended['memory_swap'] = None
332
                    try:
333
                        extended['tcp'] = len(top_process.connections(kind="tcp"))
334
                        extended['udp'] = len(top_process.connections(kind="udp"))
335
                    except (psutil.AccessDenied, psutil.NoSuchProcess):
336
                        # Manage issue1283 (psutil.AccessDenied)
337
                        extended['tcp'] = None
338
                        extended['udp'] = None
339
                except (psutil.NoSuchProcess, ValueError, AttributeError) as e:
340
                    logger.error('Can not grab extended stats ({})'.format(e))
341
                    extended['extended_stats'] = False
342
                else:
343
                    logger.debug('Grab extended stats for process {}'.format(proc['pid']))
344
                    extended['extended_stats'] = True
345
                proc.update(extended)
346
            first = False
347
            # /End of extended stats
348
349
            # PID is the key
350
            proc['key'] = 'pid'
351
352
            # Time since last update (for disk_io rate computation)
353
            proc['time_since_update'] = time_since_update
354
355
            # Process status (only keep the first char)
356
            proc['status'] = str(proc['status'])[:1].upper()
357
358
            # Process IO
359
            # procstat['io_counters'] is a list:
360
            # [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag]
361
            # If io_tag = 0 > Access denied or first time (display "?")
362
            # If io_tag = 1 > No access denied (display the IO rate)
363
            if 'io_counters' in proc and proc['io_counters'] is not None:
364
                io_new = [proc['io_counters'].read_bytes, proc['io_counters'].write_bytes]
365
                # For IO rate computation
366
                # Append saved IO r/w bytes
367
                try:
368
                    proc['io_counters'] = io_new + self.io_old[proc['pid']]
369
                    io_tag = 1
370
                except KeyError:
371
                    proc['io_counters'] = io_new + [0, 0]
372
                    io_tag = 0
373
                # then save the IO r/w bytes
374
                self.io_old[proc['pid']] = io_new
375
            else:
376
                proc['io_counters'] = [0, 0] + [0, 0]
377
                io_tag = 0
378
            # Append the IO tag (for display)
379
            proc['io_counters'] += [io_tag]
380
381
            # Manage cached information
382
            if is_cached:
383
                # Grab cached values (in case of a new incoming process)
384
                if proc['pid'] not in self.processlist_cache:
385
                    try:
386
                        self.processlist_cache[proc['pid']] = psutil.Process(pid=proc['pid']).as_dict(
387
                            attrs=cached_attrs, ad_value=None
388
                        )
389
                    except psutil.NoSuchProcess:
390
                        pass
391
                # Add cached value to current stat
392
                try:
393
                    proc.update(self.processlist_cache[proc['pid']])
394
                except KeyError:
395
                    pass
396
            else:
397
                # Save values to cache
398
                self.processlist_cache[proc['pid']] = {cached: proc[cached] for cached in cached_attrs}
399
400
        # Apply filter
401
        self.processlist = [p for p in self.processlist if not (self._filter.is_filtered(p))]
402
403
        # Compute the maximum value for keys in self._max_values_list: CPU, MEM
404
        # Useful to highlight the processes with maximum values
405
        for k in self._max_values_list:
406
            values_list = [i[k] for i in self.processlist if i[k] is not None]
407
            if values_list:
408
                self.set_max_values(k, max(values_list))
409
410
    def get_count(self):
411
        """Get the number of processes."""
412
        return self.processcount
413
414
    def getlist(self, sorted_by=None):
415
        """Get the processlist."""
416
        return self.processlist
417
418
    @property
419
    def sort_key(self):
420
        """Get the current sort key."""
421
        return self._sort_key
422
423
    def set_sort_key(self, key, auto=True):
424
        """Set the current sort key."""
425
        if key == 'auto':
426
            self.auto_sort = True
427
            self._sort_key = 'cpu_percent'
428
        else:
429
            self.auto_sort = auto
430
            self._sort_key = key
431
432
    def nice_decrease(self, pid):
433
        """ Decrease nice level
434
        On UNIX this is a number which usually goes from -20 to 20.
435
        The higher the nice value, the lower the priority of the process."""
436
        p = psutil.Process(pid)
437
        try:
438
            p.nice(p.nice() - 1)
439
            logger.info('Set nice level of process {} to {} (higher the priority)'.format(pid, p.nice()))
440
        except psutil.AccessDenied:
441
            logger.warning('Can not decrease (higher the priority) the nice level of process {} (access denied)'.format(pid))
442
443
    def nice_increase(self, pid):
444
        """ Increase nice level
445
        On UNIX this is a number which usually goes from -20 to 20.
446
        The higher the nice value, the lower the priority of the process."""
447
        p = psutil.Process(pid)
448
        try:
449
            p.nice(p.nice() + 1)
450
            logger.info('Set nice level of process {} to {} (lower the priority)'.format(pid, p.nice()))
451
        except psutil.AccessDenied:
452
            logger.warning('Can not increase (lower the priority) the nice level of process {} (access denied)'.format(pid))
453
454
    def kill(self, pid, timeout=3):
455
        """Kill process with pid"""
456
        assert pid != os.getpid(), "Glances can kill itself..."
457
        p = psutil.Process(pid)
458
        logger.debug('Send kill signal to process: {}'.format(p))
459
        p.kill()
460
        return p.wait(timeout)
461
462
463
464
465
def weighted(value):
466
    """Manage None value in dict value."""
467
    return -float('inf') if value is None else value
468
469
470
def _sort_io_counters(process, sorted_by='io_counters', sorted_by_secondary='memory_percent'):
471
    """Specific case for io_counters
472
473
    :return: Sum of io_r + io_w
474
    """
475
    return process[sorted_by][0] - process[sorted_by][2] + process[sorted_by][1] - process[sorted_by][3]
476
477
478
def _sort_cpu_times(process, sorted_by='cpu_times', sorted_by_secondary='memory_percent'):
479
    """Specific case for cpu_times
480
481
    Patch for "Sorting by process time works not as expected #1321"
482
    By default PsUtil only takes user time into account
483
    see (https://github.com/giampaolo/psutil/issues/1339)
484
    The following implementation takes user and system time into account
485
    """
486
    return process[sorted_by][0] + process[sorted_by][1]
487
488
489
def _sort_lambda(sorted_by='cpu_percent', sorted_by_secondary='memory_percent'):
490
    """Return a sort lambda function for the sorted_by key"""
491
    ret = None
492
    if sorted_by == 'io_counters':
493
        ret = _sort_io_counters
494
    elif sorted_by == 'cpu_times':
495
        ret = _sort_cpu_times
496
    return ret
497
498
499
def sort_stats(stats, sorted_by='cpu_percent', sorted_by_secondary='memory_percent', reverse=True):
500
    """Return the stats (dict) sorted by (sorted_by).
501
502
    Reverse the sort if reverse is True.
503
    """
504
    if sorted_by is None and sorted_by_secondary is None:
505
        # No need to sort...
506
        return stats
507
508
    # Check if a specific sort should be done
509
    sort_lambda = _sort_lambda(sorted_by=sorted_by, sorted_by_secondary=sorted_by_secondary)
510
511
    if sort_lambda is not None:
512
        # Specific sort
513
        try:
514
            stats.sort(key=sort_lambda, reverse=reverse)
515
        except Exception:
516
            # If an error is detected, fallback to cpu_percent
517
            stats.sort(
518
                key=lambda process: (weighted(process['cpu_percent']), weighted(process[sorted_by_secondary])),
519
                reverse=reverse,
520
            )
521
    else:
522
        # Standard sort
523
        try:
524
            stats.sort(
525
                key=lambda process: (weighted(process[sorted_by]), weighted(process[sorted_by_secondary])),
526
                reverse=reverse,
527
            )
528
        except (KeyError, TypeError):
529
            # Fallback to name
530
            stats.sort(key=lambda process: process['name'] if process['name'] is not None else '~', reverse=False)
531
532
    return stats
533
534
535
glances_processes = GlancesProcesses()
536