Test Failed
Push — master ( 7e7379...128504 )
by Nicolas
03:31
created

glances.processes.GlancesProcesses.getcount()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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