Completed
Push — master ( 3dcd25...20576f )
by Nicolas
01:26
created

sort_stats()   D

Complexity

Conditions 8

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
c 1
b 0
f 0
dl 0
loc 32
rs 4
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of Glances.
4
#
5
# Copyright (C) 2016 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
22
import re
23
24
from glances.compat import iteritems, itervalues, listitems
25
from glances.globals import BSD, LINUX, OSX, WINDOWS
26
from glances.timer import Timer, getTimeSinceLastUpdate
27
from glances.processes_tree import ProcessTreeNode
28
from glances.filter import GlancesFilter
29
from glances.logger import logger
30
31
import psutil
32
33
34
def is_kernel_thread(proc):
35
    """Return True if proc is a kernel thread, False instead."""
36
    try:
37
        return os.getpgid(proc.pid) == 0
38
    # Python >= 3.3 raises ProcessLookupError, which inherits OSError
39
    except OSError:
40
        # return False is process is dead
41
        return False
42
43
44
class GlancesProcesses(object):
45
46
    """Get processed stats using the psutil library."""
47
48
    def __init__(self, cache_timeout=60):
49
        """Init the class to collect stats about processes."""
50
        # Add internals caches because PSUtil do not cache all the stats
51
        # See: https://code.google.com/p/psutil/issues/detail?id=462
52
        self.username_cache = {}
53
        self.cmdline_cache = {}
54
55
        # The internals caches will be cleaned each 'cache_timeout' seconds
56
        self.cache_timeout = cache_timeout
57
        self.cache_timer = Timer(self.cache_timeout)
58
59
        # Init the io dict
60
        # key = pid
61
        # value = [ read_bytes_old, write_bytes_old ]
62
        self.io_old = {}
63
64
        # Wether or not to enable process tree
65
        self._enable_tree = False
66
        self.process_tree = None
67
68
        # Init stats
69
        self.auto_sort = True
70
        self._sort_key = 'cpu_percent'
71
        self.allprocesslist = []
72
        self.processlist = []
73
        self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0}
74
75
        # Tag to enable/disable the processes stats (to reduce the Glances CPU consumption)
76
        # Default is to enable the processes stats
77
        self.disable_tag = False
78
79
        # Extended stats for top process is enable by default
80
        self.disable_extended_tag = False
81
82
        # Maximum number of processes showed in the UI (None if no limit)
83
        self._max_processes = None
84
85
        # Process filter is a regular expression
86
        self._filter = GlancesFilter()
87
88
        # Whether or not to hide kernel threads
89
        self.no_kernel_threads = False
90
91
    def enable(self):
92
        """Enable process stats."""
93
        self.disable_tag = False
94
        self.update()
95
96
    def disable(self):
97
        """Disable process stats."""
98
        self.disable_tag = True
99
100
    def enable_extended(self):
101
        """Enable extended process stats."""
102
        self.disable_extended_tag = False
103
        self.update()
104
105
    def disable_extended(self):
106
        """Disable extended process stats."""
107
        self.disable_extended_tag = True
108
109
    @property
110
    def max_processes(self):
111
        """Get the maximum number of processes showed in the UI."""
112
        return self._max_processes
113
114
    @max_processes.setter
115
    def max_processes(self, value):
116
        """Set the maximum number of processes showed in the UI."""
117
        self._max_processes = value
118
119
    @property
120
    def process_filter_input(self):
121
        """Get the process filter (given by the user)."""
122
        return self._filter.filter_input
123
124
    @property
125
    def process_filter(self):
126
        """Get the process filter (current apply filter)."""
127
        return self._filter.filter
128
129
    @process_filter.setter
130
    def process_filter(self, value):
131
        """Set the process filter."""
132
        self._filter.filter = value
133
134
    @property
135
    def process_filter_key(self):
136
        """Get the process filter key."""
137
        return self._filter.filter_key
138
139
    @property
140
    def process_filter_re(self):
141
        """Get the process regular expression compiled."""
142
        return self._filter.filter_re
143
144
    def disable_kernel_threads(self):
145
        """Ignore kernel threads in process list."""
146
        self.no_kernel_threads = True
147
148
    def enable_tree(self):
149
        """Enable process tree."""
150
        self._enable_tree = True
151
152
    def is_tree_enabled(self):
153
        """Return True if process tree is enabled, False instead."""
154
        return self._enable_tree
155
156
    @property
157
    def sort_reverse(self):
158
        """Return True to sort processes in reverse 'key' order, False instead."""
159
        if self.sort_key == 'name' or self.sort_key == 'username':
160
            return False
161
162
        return True
163
164
    def __get_mandatory_stats(self, proc, procstat):
165
        """
166
        Get mandatory_stats: need for the sorting/filter step.
167
168
        => cpu_percent, memory_percent, io_counters, name, cmdline
169
        """
170
        procstat['mandatory_stats'] = True
171
172
        # Process CPU, MEM percent and name
173
        try:
174
            procstat.update(proc.as_dict(
175
                attrs=['username', 'cpu_percent', 'memory_percent',
176
                       'name', 'cpu_times'], ad_value=''))
177
        except psutil.NoSuchProcess:
178
            # Try/catch for issue #432
179
            return None
180
        if procstat['cpu_percent'] == '' or procstat['memory_percent'] == '':
181
            # Do not display process if we cannot get the basic
182
            # cpu_percent or memory_percent stats
183
            return None
184
185
        # Process command line (cached with internal cache)
186
        try:
187
            self.cmdline_cache[procstat['pid']]
188
        except KeyError:
189
            # Patch for issue #391
190
            try:
191
                self.cmdline_cache[procstat['pid']] = proc.cmdline()
192
            except (AttributeError, UnicodeDecodeError, psutil.AccessDenied, psutil.NoSuchProcess):
193
                self.cmdline_cache[procstat['pid']] = ""
194
        procstat['cmdline'] = self.cmdline_cache[procstat['pid']]
195
196
        # Process IO
197
        # procstat['io_counters'] is a list:
198
        # [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag]
199
        # If io_tag = 0 > Access denied (display "?")
200
        # If io_tag = 1 > No access denied (display the IO rate)
201
        # Note Disk IO stat not available on Mac OS
202
        if not OSX:
203
            try:
204
                # Get the process IO counters
205
                proc_io = proc.io_counters()
206
                io_new = [proc_io.read_bytes, proc_io.write_bytes]
207
            except (psutil.AccessDenied, psutil.NoSuchProcess, NotImplementedError):
208
                # Access denied to process IO (no root account)
209
                # NoSuchProcess (process die between first and second grab)
210
                # Put 0 in all values (for sort) and io_tag = 0 (for
211
                # display)
212
                procstat['io_counters'] = [0, 0] + [0, 0]
213
                io_tag = 0
214
            else:
215
                # For IO rate computation
216
                # Append saved IO r/w bytes
217
                try:
218
                    procstat['io_counters'] = io_new + \
219
                        self.io_old[procstat['pid']]
220
                except KeyError:
221
                    procstat['io_counters'] = io_new + [0, 0]
222
                # then save the IO r/w bytes
223
                self.io_old[procstat['pid']] = io_new
224
                io_tag = 1
225
226
            # Append the IO tag (for display)
227
            procstat['io_counters'] += [io_tag]
228
229
        return procstat
230
231
    def __get_standard_stats(self, proc, procstat):
232
        """
233
        Get standard_stats: for all the displayed processes.
234
235
        => username, status, memory_info, cpu_times
236
        """
237
        procstat['standard_stats'] = True
238
239
        # Process username (cached with internal cache)
240
        try:
241
            self.username_cache[procstat['pid']]
242
        except KeyError:
243
            try:
244
                self.username_cache[procstat['pid']] = proc.username()
245
            except psutil.NoSuchProcess:
246
                self.username_cache[procstat['pid']] = "?"
247
            except (KeyError, psutil.AccessDenied):
248
                try:
249
                    self.username_cache[procstat['pid']] = proc.uids().real
250
                except (KeyError, AttributeError, psutil.AccessDenied):
251
                    self.username_cache[procstat['pid']] = "?"
252
        procstat['username'] = self.username_cache[procstat['pid']]
253
254
        # Process status, nice, memory_info and cpu_times
255
        try:
256
            procstat.update(
257
                proc.as_dict(attrs=['status', 'nice', 'memory_info', 'cpu_times']))
258
        except psutil.NoSuchProcess:
259
            pass
260
        else:
261
            procstat['status'] = str(procstat['status'])[:1].upper()
262
263
        return procstat
264
265
    def __get_extended_stats(self, proc, procstat):
266
        """
267
        Get extended_stats: only for top processes (see issue #403).
268
269
        => connections (UDP/TCP), memory_swap...
270
        """
271
        procstat['extended_stats'] = True
272
273
        # CPU affinity (Windows and Linux only)
274
        try:
275
            procstat.update(proc.as_dict(attrs=['cpu_affinity']))
276
        except psutil.NoSuchProcess:
277
            pass
278
        except AttributeError:
279
            procstat['cpu_affinity'] = None
280
        # Memory extended
281
        try:
282
            procstat.update(proc.as_dict(attrs=['memory_full_info']))
283
        except psutil.NoSuchProcess:
284
            pass
285
        except AttributeError:
286
            # Fallback to standard memory_info stats
287
            try:
288
                procstat.update(proc.as_dict(attrs=['memory_info']))
289
            except psutil.NoSuchProcess:
290
                pass
291
            except AttributeError:
292
                procstat['memory_info'] = None
293
        # Number of context switch
294
        try:
295
            procstat.update(proc.as_dict(attrs=['num_ctx_switches']))
296
        except psutil.NoSuchProcess:
297
            pass
298
        except AttributeError:
299
            procstat['num_ctx_switches'] = None
300
        # Number of file descriptors (Unix only)
301
        try:
302
            procstat.update(proc.as_dict(attrs=['num_fds']))
303
        except psutil.NoSuchProcess:
304
            pass
305
        except AttributeError:
306
            procstat['num_fds'] = None
307
        # Threads number
308
        try:
309
            procstat.update(proc.as_dict(attrs=['num_threads']))
310
        except psutil.NoSuchProcess:
311
            pass
312
        except AttributeError:
313
            procstat['num_threads'] = None
314
315
        # Number of handles (Windows only)
316
        if WINDOWS:
317
            try:
318
                procstat.update(proc.as_dict(attrs=['num_handles']))
319
            except psutil.NoSuchProcess:
320
                pass
321
        else:
322
            procstat['num_handles'] = None
323
324
        # SWAP memory (Only on Linux based OS)
325
        # http://www.cyberciti.biz/faq/linux-which-process-is-using-swap/
326
        if LINUX:
327
            try:
328
                procstat['memory_swap'] = sum(
329
                    [v.swap for v in proc.memory_maps()])
330
            except psutil.NoSuchProcess:
331
                pass
332
            except psutil.AccessDenied:
333
                procstat['memory_swap'] = None
334
            except Exception:
335
                # Add a dirty except to handle the PsUtil issue #413
336
                procstat['memory_swap'] = None
337
338
        # Process network connections (TCP and UDP)
339
        try:
340
            procstat['tcp'] = len(proc.connections(kind="tcp"))
341
            procstat['udp'] = len(proc.connections(kind="udp"))
342
        except Exception:
343
            procstat['tcp'] = None
344
            procstat['udp'] = None
345
346
        # IO Nice
347
        # http://pythonhosted.org/psutil/#psutil.Process.ionice
348
        if LINUX or WINDOWS:
349
            try:
350
                procstat.update(proc.as_dict(attrs=['ionice']))
351
            except psutil.NoSuchProcess:
352
                pass
353
        else:
354
            procstat['ionice'] = None
355
356
        return procstat
357
358
    def __get_process_stats(self, proc,
359
                            mandatory_stats=True,
360
                            standard_stats=True,
361
                            extended_stats=False):
362
        """Get stats of running processes."""
363
        # Process ID (always)
364
        procstat = proc.as_dict(attrs=['pid'])
365
366
        if mandatory_stats:
367
            procstat = self.__get_mandatory_stats(proc, procstat)
368
369
        if procstat is not None and standard_stats:
370
            procstat = self.__get_standard_stats(proc, procstat)
371
372
        if procstat is not None and extended_stats and not self.disable_extended_tag:
373
            procstat = self.__get_extended_stats(proc, procstat)
374
375
        return procstat
376
377
    def update(self):
378
        """Update the processes stats."""
379
        # Reset the stats
380
        self.processlist = []
381
        self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0}
382
383
        # Do not process if disable tag is set
384
        if self.disable_tag:
385
            return
386
387
        # Get the time since last update
388
        time_since_update = getTimeSinceLastUpdate('process_disk')
389
390
        # Build an internal dict with only mandatories stats (sort keys)
391
        processdict = {}
392
        excluded_processes = set()
393
        for proc in psutil.process_iter():
394
            # Ignore kernel threads if needed
395
            if self.no_kernel_threads and not WINDOWS and is_kernel_thread(proc):
396
                continue
397
398
            # If self.max_processes is None: Only retrieve mandatory stats
399
            # Else: retrieve mandatory and standard stats
400
            s = self.__get_process_stats(proc,
401
                                         mandatory_stats=True,
402
                                         standard_stats=self.max_processes is None)
403
            # Check if s is note None (issue #879)
404
            # ignore the 'idle' process on Windows and *BSD
405
            # ignore the 'kernel_task' process on OS X
406
            # waiting for upstream patch from psutil
407
            if (s is None or
408
               BSD and s['name'] == 'idle' or
409
               WINDOWS and s['name'] == 'System Idle Process' or
410
               OSX and s['name'] == 'kernel_task'):
411
                continue
412
            # Continue to the next process if it has to be filtered
413
            if self._filter.is_filtered(s):
414
                excluded_processes.add(proc)
415
                continue
416
417
            # Ok add the process to the list
418
            processdict[proc] = s
419
            # Update processcount (global statistics)
420
            try:
421
                self.processcount[str(proc.status())] += 1
422
            except KeyError:
423
                # Key did not exist, create it
424
                try:
425
                    self.processcount[str(proc.status())] = 1
426
                except psutil.NoSuchProcess:
427
                    pass
428
            except psutil.NoSuchProcess:
429
                pass
430
            else:
431
                self.processcount['total'] += 1
432
            # Update thread number (global statistics)
433
            try:
434
                self.processcount['thread'] += proc.num_threads()
435
            except Exception:
436
                pass
437
438
        if self._enable_tree:
439
            self.process_tree = ProcessTreeNode.build_tree(processdict,
440
                                                           self.sort_key,
441
                                                           self.sort_reverse,
442
                                                           self.no_kernel_threads,
443
                                                           excluded_processes)
444
445
            for i, node in enumerate(self.process_tree):
446
                # Only retreive stats for visible processes (max_processes)
447
                if self.max_processes is not None and i >= self.max_processes:
448
                    break
449
450
                # add standard stats
451
                new_stats = self.__get_process_stats(node.process,
452
                                                     mandatory_stats=False,
453
                                                     standard_stats=True,
454
                                                     extended_stats=False)
455
                if new_stats is not None:
456
                    node.stats.update(new_stats)
457
458
                # Add a specific time_since_update stats for bitrate
459
                node.stats['time_since_update'] = time_since_update
460
461
        else:
462
            # Process optimization
463
            # Only retreive stats for visible processes (max_processes)
464
            if self.max_processes is not None:
465
                # Sort the internal dict and cut the top N (Return a list of tuple)
466
                # tuple=key (proc), dict (returned by __get_process_stats)
467
                try:
468
                    processiter = sorted(iteritems(processdict),
469
                                         key=lambda x: x[1][self.sort_key],
470
                                         reverse=self.sort_reverse)
471
                except (KeyError, TypeError) as e:
472
                    logger.error("Cannot sort process list by {}: {}".format(self.sort_key, e))
473
                    logger.error('{}'.format(listitems(processdict)[0]))
474
                    # Fallback to all process (issue #423)
475
                    processloop = iteritems(processdict)
476
                    first = False
477
                else:
478
                    processloop = processiter[0:self.max_processes]
479
                    first = True
480
            else:
481
                # Get all processes stats
482
                processloop = iteritems(processdict)
483
                first = False
484
485
            for i in processloop:
486
                # Already existing mandatory stats
487
                procstat = i[1]
488
                if self.max_processes is not None:
489
                    # Update with standard stats
490
                    # and extended stats but only for TOP (first) process
491
                    s = self.__get_process_stats(i[0],
492
                                                 mandatory_stats=False,
493
                                                 standard_stats=True,
494
                                                 extended_stats=first)
495
                    if s is None:
496
                        continue
497
                    procstat.update(s)
498
                # Add a specific time_since_update stats for bitrate
499
                procstat['time_since_update'] = time_since_update
500
                # Update process list
501
                self.processlist.append(procstat)
502
                # Next...
503
                first = False
504
505
        # Build the all processes list used by the AMPs
506
        self.allprocesslist = [p for p in itervalues(processdict)]
507
508
        # Clean internals caches if timeout is reached
509
        if self.cache_timer.finished():
510
            self.username_cache = {}
511
            self.cmdline_cache = {}
512
            # Restart the timer
513
            self.cache_timer.reset()
514
515
    def getcount(self):
516
        """Get the number of processes."""
517
        return self.processcount
518
519
    def getalllist(self):
520
        """Get the allprocesslist."""
521
        return self.allprocesslist
522
523
    def getlist(self, sortedby=None):
524
        """Get the processlist."""
525
        return self.processlist
526
527
    def gettree(self):
528
        """Get the process tree."""
529
        return self.process_tree
530
531
    @property
532
    def sort_key(self):
533
        """Get the current sort key."""
534
        return self._sort_key
535
536
    @sort_key.setter
537
    def sort_key(self, key):
538
        """Set the current sort key."""
539
        self._sort_key = key
540
541
542
# TODO: move this global function (also used in glances_processlist
543
#       and logs) inside the GlancesProcesses class
544
def sort_stats(stats, sortedby=None, tree=False, reverse=True):
545
    """Return the stats (dict) sorted by (sortedby)
546
    Reverse the sort if reverse is True."""
547
    if sortedby is None:
548
        # No need to sort...
549
        return stats
550
551
    if sortedby == 'io_counters' and not tree:
552
        # Specific case for io_counters
553
        # Sum of io_r + io_w
554
        try:
555
            # Sort process by IO rate (sum IO read + IO write)
556
            stats.sort(key=lambda process: process[sortedby][0] -
557
                       process[sortedby][2] + process[sortedby][1] -
558
                       process[sortedby][3],
559
                       reverse=reverse)
560
        except Exception:
561
            stats.sort(key=operator.itemgetter('cpu_percent'),
562
                       reverse=reverse)
563
    else:
564
        # Others sorts
565
        if tree:
566
            stats.set_sorting(sortedby, reverse)
567
        else:
568
            try:
569
                stats.sort(key=operator.itemgetter(sortedby),
570
                           reverse=reverse)
571
            except (KeyError, TypeError):
572
                stats.sort(key=operator.itemgetter('name'),
573
                           reverse=False)
574
575
    return stats
576
577
578
glances_processes = GlancesProcesses()
579