Completed
Pull Request — master (#963)
by
unknown
01:02
created

GlancesProcesses.reset_max_values()   A

Complexity

Conditions 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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