Test Failed
Push — develop ( d7cf39...faa4bd )
by Nicolas
04:34 queued 10s
created

glances/processes.py (3 issues)

1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of Glances.
4
#
5
# Copyright (C) 2019 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 operator
21
import os
0 ignored issues
show
import missing from __future__ import absolute_import
Loading history...
The import os seems to be unused.
Loading history...
22
23
from glances.compat import iteritems, itervalues, listitems, iterkeys
24
from glances.globals import BSD, LINUX, MACOS, SUNOS, WINDOWS, WSL
25
from glances.timer import Timer, getTimeSinceLastUpdate
26
from glances.filter import GlancesFilter
27
from glances.logger import logger
28
29
import psutil
30
31
32
class GlancesProcesses(object):
33
34
    """Get processed stats using the psutil library."""
35
36
    def __init__(self, cache_timeout=60):
37
        """Init the class to collect stats about processes."""
38
        # Add internals caches because psutil do not cache all the stats
39
        # See: https://code.google.com/p/psutil/issues/detail?id=462
40
        self.username_cache = {}
41
        self.cmdline_cache = {}
42
43
        # The internals caches will be cleaned each 'cache_timeout' seconds
44
        self.cache_timeout = cache_timeout
45
        self.cache_timer = Timer(self.cache_timeout)
46
47
        # Init the io dict
48
        # key = pid
49
        # value = [ read_bytes_old, write_bytes_old ]
50
        self.io_old = {}
51
52
        # Init stats
53
        self.auto_sort = True
54
        self._sort_key = 'cpu_percent'
55
        self.processlist = []
56
        self.reset_processcount()
57
58
        # Tag to enable/disable the processes stats (to reduce the Glances CPU consumption)
0 ignored issues
show
This line is too long as per the coding-style (91/80).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
59
        # Default is to enable the processes stats
60
        self.disable_tag = False
61
62
        # Extended stats for top process is enable by default
63
        self.disable_extended_tag = False
64
65
        # Maximum number of processes showed in the UI (None if no limit)
66
        self._max_processes = None
67
68
        # Process filter is a regular expression
69
        self._filter = GlancesFilter()
70
71
        # Whether or not to hide kernel threads
72
        self.no_kernel_threads = False
73
74
        # Store maximums values in a dict
75
        # Used in the UI to highlight the maximum value
76
        self._max_values_list = ('cpu_percent', 'memory_percent')
77
        # { 'cpu_percent': 0.0, 'memory_percent': 0.0 }
78
        self._max_values = {}
79
        self.reset_max_values()
80
81
    def reset_processcount(self):
82
        """Reset the global process count"""
83
        self.processcount = {'total': 0,
84
                             'running': 0,
85
                             'sleeping': 0,
86
                             'thread': 0,
87
                             'pid_max': None}
88
89
    def update_processcount(self, plist):
90
        """Update the global process count from the current processes list"""
91
        # Update the maximum process ID (pid) number
92
        self.processcount['pid_max'] = self.pid_max
93
        # For each key in the processcount dict
94
        # count the number of processes with the same status
95
        for k in iterkeys(self.processcount):
96
            self.processcount[k] = len(list(filter(lambda v: v['status'] is k,
97
                                                   plist)))
98
        # Compute thread
99
        self.processcount['thread'] = sum(i['num_threads'] for i in plist
100
                                          if i['num_threads'] is not None)
101
        # Compute total
102
        self.processcount['total'] = len(plist)
103
104
    def enable(self):
105
        """Enable process stats."""
106
        self.disable_tag = False
107
        self.update()
108
109
    def disable(self):
110
        """Disable process stats."""
111
        self.disable_tag = True
112
113
    def enable_extended(self):
114
        """Enable extended process stats."""
115
        self.disable_extended_tag = False
116
        self.update()
117
118
    def disable_extended(self):
119
        """Disable extended process stats."""
120
        self.disable_extended_tag = True
121
122
    @property
123
    def pid_max(self):
124
        """
125
        Get the maximum PID value.
126
127
        On Linux, the value is read from the `/proc/sys/kernel/pid_max` file.
128
129
        From `man 5 proc`:
130
        The default value for this file, 32768, results in the same range of
131
        PIDs as on earlier kernels. On 32-bit platfroms, 32768 is the maximum
132
        value for pid_max. On 64-bit systems, pid_max can be set to any value
133
        up to 2^22 (PID_MAX_LIMIT, approximately 4 million).
134
135
        If the file is unreadable or not available for whatever reason,
136
        returns None.
137
138
        Some other OSes:
139
        - On FreeBSD and macOS the maximum is 99999.
140
        - On OpenBSD >= 6.0 the maximum is 99999 (was 32766).
141
        - On NetBSD the maximum is 30000.
142
143
        :returns: int or None
144
        """
145
        if LINUX:
146
            # XXX: waiting for https://github.com/giampaolo/psutil/issues/720
147
            try:
148
                with open('/proc/sys/kernel/pid_max', 'rb') as f:
149
                    return int(f.read())
150
            except (OSError, IOError):
151
                return None
152
        else:
153
            return None
154
155
    @property
156
    def max_processes(self):
157
        """Get the maximum number of processes showed in the UI."""
158
        return self._max_processes
159
160
    @max_processes.setter
161
    def max_processes(self, value):
162
        """Set the maximum number of processes showed in the UI."""
163
        self._max_processes = value
164
165
    @property
166
    def process_filter_input(self):
167
        """Get the process filter (given by the user)."""
168
        return self._filter.filter_input
169
170
    @property
171
    def process_filter(self):
172
        """Get the process filter (current apply filter)."""
173
        return self._filter.filter
174
175
    @process_filter.setter
176
    def process_filter(self, value):
177
        """Set the process filter."""
178
        self._filter.filter = value
179
180
    @property
181
    def process_filter_key(self):
182
        """Get the process filter key."""
183
        return self._filter.filter_key
184
185
    @property
186
    def process_filter_re(self):
187
        """Get the process regular expression compiled."""
188
        return self._filter.filter_re
189
190
    def disable_kernel_threads(self):
191
        """Ignore kernel threads in process list."""
192
        self.no_kernel_threads = True
193
194
    @property
195
    def sort_reverse(self):
196
        """Return True to sort processes in reverse 'key' order, False instead."""
197
        if self.sort_key == 'name' or self.sort_key == 'username':
198
            return False
199
200
        return True
201
202
    def max_values(self):
203
        """Return the max values dict."""
204
        return self._max_values
205
206
    def get_max_values(self, key):
207
        """Get the maximum values of the given stat (key)."""
208
        return self._max_values[key]
209
210
    def set_max_values(self, key, value):
211
        """Set the maximum value for a specific stat (key)."""
212
        self._max_values[key] = value
213
214
    def reset_max_values(self):
215
        """Reset the maximum values dict."""
216
        self._max_values = {}
217
        for k in self._max_values_list:
218
            self._max_values[k] = 0.0
219
220
    def update(self):
221
        """Update the processes stats."""
222
        # Reset the stats
223
        self.processlist = []
224
        self.reset_processcount()
225
226
        # Do not process if disable tag is set
227
        if self.disable_tag:
228
            return
229
230
        # Time since last update (for disk_io rate computation)
231
        time_since_update = getTimeSinceLastUpdate('process_disk')
232
233
        # Grab standard stats
234
        #####################
235
        standard_attrs = ['cmdline', 'cpu_percent', 'cpu_times', 'memory_info',
236
                          'memory_percent', 'name', 'nice', 'pid', 'ppid',
237
                          'status', 'username', 'status', 'num_threads']
238
        # io_counters availability: Linux, BSD, Windows, AIX
239
        if not MACOS and not SUNOS and not WSL:
240
            standard_attrs += ['io_counters']
241
        # gids availability: Unix
242
        if not WINDOWS:
243
            standard_attrs += ['gids']
244
245
        # and build the processes stats list (psutil>=5.3.0)
246
        self.processlist = [p.info for p in psutil.process_iter(attrs=standard_attrs,
247
                                                                ad_value=None)
248
                            # OS-related processes filter
249
                            if not (BSD and p.info['name'] == 'idle') and
250
                            not (WINDOWS and p.info['name'] == 'System Idle Process') and
251
                            not (MACOS and p.info['name'] == 'kernel_task') and
252
                            # Kernel threads filter
253
                            not (self.no_kernel_threads and LINUX and p.info['gids'].real == 0) and
254
                            # User filter
255
                            not (self._filter.is_filtered(p.info))]
256
257
        # Sort the processes list by the current sort_key
258
        self.processlist = sort_stats(self.processlist,
259
                                      sortedby=self.sort_key,
260
                                      reverse=True)
261
262
        # Update the processcount
263
        self.update_processcount(self.processlist)
264
265
        # Loop over processes and add metadata
266
        first = True
267
        for proc in self.processlist:
268
            # Get extended stats, only for top processes (see issue #403).
269
            if first and not self.disable_extended_tag:
270
                # - cpu_affinity (Linux, Windows, FreeBSD)
271
                # - ionice (Linux and Windows > Vista)
272
                # - num_ctx_switches (not available on Illumos/Solaris)
273
                # - num_fds (Unix-like)
274
                # - num_handles (Windows)
275
                # - memory_maps (only swap, Linux)
276
                #   https://www.cyberciti.biz/faq/linux-which-process-is-using-swap/
277
                # - connections (TCP and UDP)
278
                extended = {}
279
                try:
280
                    top_process = psutil.Process(proc['pid'])
281
                    extended_stats = ['cpu_affinity', 'ionice',
282
                                      'num_ctx_switches']
283
                    if LINUX:
284
                        # num_fds only avalable on Unix system (see issue #1351)
285
                        extended_stats += ['num_fds']
286
                    if WINDOWS:
287
                        extended_stats += ['num_handles']
288
289
                    # Get the extended stats
290
                    extended = top_process.as_dict(attrs=extended_stats,
291
                                                   ad_value=None)
292
293
                    if LINUX:
294
                        try:
295
                            extended['memory_swap'] = sum([v.swap for v in top_process.memory_maps()])
296
                        except psutil.NoSuchProcess:
297
                            pass
298
                        except (psutil.AccessDenied, NotImplementedError):
299
                            # NotImplementedError: /proc/${PID}/smaps file doesn't exist
300
                            # on kernel < 2.6.14 or CONFIG_MMU kernel configuration option
301
                            # is not enabled (see psutil #533/glances #413).
302
                            extended['memory_swap'] = None
303
                    try:
304
                        extended['tcp'] = len(top_process.connections(kind="tcp"))
305
                        extended['udp'] = len(top_process.connections(kind="udp"))
306
                    except (psutil.AccessDenied, psutil.NoSuchProcess):
307
                        # Manage issue1283 (psutil.AccessDenied)
308
                        extended['tcp'] = None
309
                        extended['udp'] = None
310
                except (psutil.NoSuchProcess, ValueError, AttributeError) as e:
311
                    logger.error('Can not grab extended stats ({})'.format(e))
312
                    extended['extended_stats'] = False
313
                else:
314
                    logger.debug('Grab extended stats for process {}'.format(proc['pid']))
315
                    extended['extended_stats'] = True
316
                proc.update(extended)
317
            first = False
318
            # /End of extended stats
319
320
            # Time since last update (for disk_io rate computation)
321
            proc['time_since_update'] = time_since_update
322
323
            # Process status (only keep the first char)
324
            proc['status'] = str(proc['status'])[:1].upper()
325
326
            # Process IO
327
            # procstat['io_counters'] is a list:
328
            # [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag]
329
            # If io_tag = 0 > Access denied or first time (display "?")
330
            # If io_tag = 1 > No access denied (display the IO rate)
331
            if 'io_counters' in proc and proc['io_counters'] is not None:
332
                io_new = [proc['io_counters'].read_bytes,
333
                          proc['io_counters'].write_bytes]
334
                # For IO rate computation
335
                # Append saved IO r/w bytes
336
                try:
337
                    proc['io_counters'] = io_new + self.io_old[proc['pid']]
338
                    io_tag = 1
339
                except KeyError:
340
                    proc['io_counters'] = io_new + [0, 0]
341
                    io_tag = 0
342
                # then save the IO r/w bytes
343
                self.io_old[proc['pid']] = io_new
344
            else:
345
                proc['io_counters'] = [0, 0] + [0, 0]
346
                io_tag = 0
347
            # Append the IO tag (for display)
348
            proc['io_counters'] += [io_tag]
349
350
        # Compute the maximum value for keys in self._max_values_list: CPU, MEM
351
        # Usefull to highlight the processes with maximum values
352
        for k in self._max_values_list:
353
            values_list = [i[k] for i in self.processlist if i[k] is not None]
354
            if values_list != []:
355
                self.set_max_values(k, max(values_list))
356
357
    def getcount(self):
358
        """Get the number of processes."""
359
        return self.processcount
360
361
    def getlist(self, sortedby=None):
362
        """Get the processlist."""
363
        return self.processlist
364
365
    @property
366
    def sort_key(self):
367
        """Get the current sort key."""
368
        return self._sort_key
369
370
    @sort_key.setter
371
    def sort_key(self, key):
372
        """Set the current sort key."""
373
        self._sort_key = key
374
375
376
def weighted(value):
377
    """Manage None value in dict value."""
378
    return -float('inf') if value is None else value
379
380
381
def _sort_io_counters(process,
382
                      sortedby='io_counters',
383
                      sortedby_secondary='memory_percent'):
384
    """Specific case for io_counters
385
    Sum of io_r + io_w"""
386
    return process[sortedby][0] - process[sortedby][2] + process[sortedby][1] - process[sortedby][3]
387
388
389
def _sort_cpu_times(process,
390
                    sortedby='cpu_times',
391
                    sortedby_secondary='memory_percent'):
392
    """ Specific case for cpu_times
393
    Patch for "Sorting by process time works not as expected #1321"
394
    By default PsUtil only takes user time into account
395
    see (https://github.com/giampaolo/psutil/issues/1339)
396
    The following implementation takes user and system time into account"""
397
    return process[sortedby][0] + process[sortedby][1]
398
399
400
def _sort_lambda(sortedby='cpu_percent',
401
                 sortedby_secondary='memory_percent'):
402
    """Return a sort lambda function for the sortedbykey"""
403
    ret = None
404
    if sortedby == 'io_counters':
405
        ret = _sort_io_counters
406
    elif sortedby == 'cpu_times':
407
        ret = _sort_cpu_times
408
    return ret
409
410
411
def sort_stats(stats,
412
               sortedby='cpu_percent',
413
               sortedby_secondary='memory_percent',
414
               reverse=True):
415
    """Return the stats (dict) sorted by (sortedby).
416
417
    Reverse the sort if reverse is True.
418
    """
419
    if sortedby is None and sortedby_secondary is None:
420
        # No need to sort...
421
        return stats
422
423
    # Check if a specific sort should be done
424
    sort_lambda = _sort_lambda(sortedby=sortedby,
425
                               sortedby_secondary=sortedby_secondary)
426
427
    if sort_lambda is not None:
428
        # Specific sort
429
        try:
430
            stats.sort(key=sort_lambda, reverse=reverse)
431
        except Exception:
432
            # If an error is detected, fallback to cpu_percent
433
            stats.sort(key=lambda process: (weighted(process['cpu_percent']),
434
                                            weighted(process[sortedby_secondary])),
435
                       reverse=reverse)
436
    else:
437
        # Standard sort
438
        try:
439
            stats.sort(key=lambda process: (weighted(process[sortedby]),
440
                                            weighted(process[sortedby_secondary])),
441
                       reverse=reverse)
442
        except (KeyError, TypeError):
443
            # Fallback to name
444
            stats.sort(key=lambda process: process['name'] if process['name'] is not None else '~',
445
                       reverse=False)
446
447
    return stats
448
449
450
glances_processes = GlancesProcesses()
451