processes.py 20.6 KB
Newer Older
A
Alessio Sergi 已提交
1 2
# -*- coding: utf-8 -*-
#
3
# This file is part of Glances.
A
Alessio Sergi 已提交
4
#
N
nicolargo 已提交
5
# Copyright (C) 2016 Nicolargo <nicolas@nicolargo.com>
A
Alessio Sergi 已提交
6 7 8 9 10 11 12 13 14 15 16 17 18 19
#
# Glances is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Glances is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

20
import operator
D
desbma 已提交
21
import os
N
Nicolargo 已提交
22
import re
A
Alessio Sergi 已提交
23

24
from glances.compat import iteritems, itervalues, listitems
A
Alessio Sergi 已提交
25
from glances.globals import BSD, LINUX, OSX, WINDOWS
26
from glances.timer import Timer, getTimeSinceLastUpdate
N
nicolargo 已提交
27
from glances.processes_tree import ProcessTreeNode
N
nicolargo 已提交
28 29
from glances.filter import GlancesFilter
from glances.logger import logger
A
flake8  
Alessio Sergi 已提交
30 31

import psutil
32

D
desbma 已提交
33

D
desbma 已提交
34
def is_kernel_thread(proc):
A
PEP 257  
Alessio Sergi 已提交
35
    """Return True if proc is a kernel thread, False instead."""
D
desbma 已提交
36 37
    try:
        return os.getpgid(proc.pid) == 0
N
nicolargo 已提交
38 39
    # Python >= 3.3 raises ProcessLookupError, which inherits OSError
    except OSError:
D
desbma 已提交
40 41 42 43
        # return False is process is dead
        return False


A
Alessio Sergi 已提交
44
class GlancesProcesses(object):
A
PEP 257  
Alessio Sergi 已提交
45 46

    """Get processed stats using the psutil library."""
A
Alessio Sergi 已提交
47 48

    def __init__(self, cache_timeout=60):
A
PEP 257  
Alessio Sergi 已提交
49
        """Init the class to collect stats about processes."""
A
Alessio Sergi 已提交
50 51 52 53
        # Add internals caches because PSUtil do not cache all the stats
        # See: https://code.google.com/p/psutil/issues/detail?id=462
        self.username_cache = {}
        self.cmdline_cache = {}
A
Alessio Sergi 已提交
54

A
Alessio Sergi 已提交
55 56 57
        # The internals caches will be cleaned each 'cache_timeout' seconds
        self.cache_timeout = cache_timeout
        self.cache_timer = Timer(self.cache_timeout)
A
Alessio Sergi 已提交
58

A
Alessio Sergi 已提交
59 60 61 62
        # Init the io dict
        # key = pid
        # value = [ read_bytes_old, write_bytes_old ]
        self.io_old = {}
A
Alessio Sergi 已提交
63

64 65
        # Wether or not to enable process tree
        self._enable_tree = False
66
        self.process_tree = None
67 68

        # Init stats
69 70
        self.auto_sort = True
        self._sort_key = 'cpu_percent'
71
        self.allprocesslist = []
A
Alessio Sergi 已提交
72
        self.processlist = []
73
        self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0}
A
Alessio Sergi 已提交
74

D
desbma 已提交
75
        # Tag to enable/disable the processes stats (to reduce the Glances CPU consumption)
76 77 78
        # Default is to enable the processes stats
        self.disable_tag = False

N
Nicolargo 已提交
79 80 81
        # Extended stats for top process is enable by default
        self.disable_extended_tag = False

82 83
        # Maximum number of processes showed in the UI (None if no limit)
        self._max_processes = None
84

N
Nicolargo 已提交
85
        # Process filter is a regular expression
N
nicolargo 已提交
86
        self._filter = GlancesFilter()
N
Nicolargo 已提交
87

88 89 90
        # Whether or not to hide kernel threads
        self.no_kernel_threads = False

91
    def enable(self):
A
PEP 257  
Alessio Sergi 已提交
92
        """Enable process stats."""
93 94 95 96
        self.disable_tag = False
        self.update()

    def disable(self):
A
PEP 257  
Alessio Sergi 已提交
97
        """Disable process stats."""
98 99
        self.disable_tag = True

N
Nicolargo 已提交
100 101 102 103 104 105 106 107 108
    def enable_extended(self):
        """Enable extended process stats."""
        self.disable_extended_tag = False
        self.update()

    def disable_extended(self):
        """Disable extended process stats."""
        self.disable_extended_tag = True

109 110 111 112
    @property
    def max_processes(self):
        """Get the maximum number of processes showed in the UI."""
        return self._max_processes
113

114 115 116 117
    @max_processes.setter
    def max_processes(self, value):
        """Set the maximum number of processes showed in the UI."""
        self._max_processes = value
118

119 120 121 122 123
    @property
    def process_filter_input(self):
        """Get the process filter (given by the user)."""
        return self._filter.filter_input

124 125
    @property
    def process_filter(self):
126
        """Get the process filter (current apply filter)."""
N
nicolargo 已提交
127
        return self._filter.filter
128 129 130 131

    @process_filter.setter
    def process_filter(self, value):
        """Set the process filter."""
N
nicolargo 已提交
132
        self._filter.filter = value
N
Nicolargo 已提交
133

134 135 136 137 138
    @property
    def process_filter_key(self):
        """Get the process filter key."""
        return self._filter.filter_key

139 140 141
    @property
    def process_filter_re(self):
        """Get the process regular expression compiled."""
N
nicolargo 已提交
142
        return self._filter.filter_re
N
Nicolargo 已提交
143

144
    def disable_kernel_threads(self):
A
PEP 257  
Alessio Sergi 已提交
145
        """Ignore kernel threads in process list."""
146 147
        self.no_kernel_threads = True

148
    def enable_tree(self):
A
PEP 257  
Alessio Sergi 已提交
149
        """Enable process tree."""
150 151 152
        self._enable_tree = True

    def is_tree_enabled(self):
A
PEP 257  
Alessio Sergi 已提交
153
        """Return True if process tree is enabled, False instead."""
154 155
        return self._enable_tree

A
Alessio Sergi 已提交
156 157 158 159 160 161 162 163
    @property
    def sort_reverse(self):
        """Return True to sort processes in reverse 'key' order, False instead."""
        if self.sort_key == 'name' or self.sort_key == 'username':
            return False

        return True

N
nicolargo 已提交
164
    def __get_mandatory_stats(self, proc, procstat):
165
        """
A
PEP 257  
Alessio Sergi 已提交
166 167
        Get mandatory_stats: need for the sorting/filter step.

N
Nicolargo 已提交
168
        => cpu_percent, memory_percent, io_counters, name, cmdline
169
        """
N
nicolargo 已提交
170 171 172
        procstat['mandatory_stats'] = True

        # Process CPU, MEM percent and name
173 174 175
        try:
            procstat.update(proc.as_dict(
                attrs=['username', 'cpu_percent', 'memory_percent',
176
                       'name', 'cpu_times'], ad_value=''))
177 178 179
        except psutil.NoSuchProcess:
            # Try/catch for issue #432
            return None
N
nicolargo 已提交
180 181 182
        if procstat['cpu_percent'] == '' or procstat['memory_percent'] == '':
            # Do not display process if we cannot get the basic
            # cpu_percent or memory_percent stats
N
nicolargo 已提交
183
            return None
N
nicolargo 已提交
184 185 186 187 188 189

        # Process command line (cached with internal cache)
        try:
            self.cmdline_cache[procstat['pid']]
        except KeyError:
            # Patch for issue #391
190
            try:
191
                self.cmdline_cache[procstat['pid']] = proc.cmdline()
N
nicolargo 已提交
192 193 194 195 196 197 198 199 200 201
            except (AttributeError, UnicodeDecodeError, psutil.AccessDenied, psutil.NoSuchProcess):
                self.cmdline_cache[procstat['pid']] = ""
        procstat['cmdline'] = self.cmdline_cache[procstat['pid']]

        # Process IO
        # procstat['io_counters'] is a list:
        # [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag]
        # If io_tag = 0 > Access denied (display "?")
        # If io_tag = 1 > No access denied (display the IO rate)
        # Note Disk IO stat not available on Mac OS
A
Alessio Sergi 已提交
202
        if not OSX:
N
Nicolargo 已提交
203
            try:
N
nicolargo 已提交
204 205 206 207 208 209 210 211 212 213 214 215 216
                # Get the process IO counters
                proc_io = proc.io_counters()
                io_new = [proc_io.read_bytes, proc_io.write_bytes]
            except (psutil.AccessDenied, psutil.NoSuchProcess, NotImplementedError):
                # Access denied to process IO (no root account)
                # NoSuchProcess (process die between first and second grab)
                # Put 0 in all values (for sort) and io_tag = 0 (for
                # display)
                procstat['io_counters'] = [0, 0] + [0, 0]
                io_tag = 0
            else:
                # For IO rate computation
                # Append saved IO r/w bytes
A
Alessio Sergi 已提交
217
                try:
N
nicolargo 已提交
218 219 220 221 222 223 224
                    procstat['io_counters'] = io_new + \
                        self.io_old[procstat['pid']]
                except KeyError:
                    procstat['io_counters'] = io_new + [0, 0]
                # then save the IO r/w bytes
                self.io_old[procstat['pid']] = io_new
                io_tag = 1
225

N
nicolargo 已提交
226 227 228 229 230 231 232
            # Append the IO tag (for display)
            procstat['io_counters'] += [io_tag]

        return procstat

    def __get_standard_stats(self, proc, procstat):
        """
A
PEP 257  
Alessio Sergi 已提交
233 234
        Get standard_stats: for all the displayed processes.

N
nicolargo 已提交
235 236 237
        => username, status, memory_info, cpu_times
        """
        procstat['standard_stats'] = True
N
Nicolargo 已提交
238

N
nicolargo 已提交
239 240 241 242
        # Process username (cached with internal cache)
        try:
            self.username_cache[procstat['pid']]
        except KeyError:
N
Nicolas Hennion 已提交
243
            try:
N
nicolargo 已提交
244 245 246 247
                self.username_cache[procstat['pid']] = proc.username()
            except psutil.NoSuchProcess:
                self.username_cache[procstat['pid']] = "?"
            except (KeyError, psutil.AccessDenied):
248
                try:
N
nicolargo 已提交
249 250
                    self.username_cache[procstat['pid']] = proc.uids().real
                except (KeyError, AttributeError, psutil.AccessDenied):
251
                    self.username_cache[procstat['pid']] = "?"
N
nicolargo 已提交
252 253 254 255 256 257 258 259 260 261
        procstat['username'] = self.username_cache[procstat['pid']]

        # Process status, nice, memory_info and cpu_times
        try:
            procstat.update(
                proc.as_dict(attrs=['status', 'nice', 'memory_info', 'cpu_times']))
        except psutil.NoSuchProcess:
            pass
        else:
            procstat['status'] = str(procstat['status'])[:1].upper()
262

N
nicolargo 已提交
263
        return procstat
N
Nicolargo 已提交
264

N
nicolargo 已提交
265 266
    def __get_extended_stats(self, proc, procstat):
        """
A
PEP 257  
Alessio Sergi 已提交
267 268
        Get extended_stats: only for top processes (see issue #403).

N
nicolargo 已提交
269 270 271 272 273 274 275 276 277 278 279 280 281
        => connections (UDP/TCP), memory_swap...
        """
        procstat['extended_stats'] = True

        # CPU affinity (Windows and Linux only)
        try:
            procstat.update(proc.as_dict(attrs=['cpu_affinity']))
        except psutil.NoSuchProcess:
            pass
        except AttributeError:
            procstat['cpu_affinity'] = None
        # Memory extended
        try:
282
            procstat.update(proc.as_dict(attrs=['memory_full_info']))
N
nicolargo 已提交
283 284 285
        except psutil.NoSuchProcess:
            pass
        except AttributeError:
286 287 288 289 290 291 292
            # Fallback to standard memory_info stats
            try:
                procstat.update(proc.as_dict(attrs=['memory_info']))
            except psutil.NoSuchProcess:
                pass
            except AttributeError:
                procstat['memory_info'] = None
N
nicolargo 已提交
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315
        # Number of context switch
        try:
            procstat.update(proc.as_dict(attrs=['num_ctx_switches']))
        except psutil.NoSuchProcess:
            pass
        except AttributeError:
            procstat['num_ctx_switches'] = None
        # Number of file descriptors (Unix only)
        try:
            procstat.update(proc.as_dict(attrs=['num_fds']))
        except psutil.NoSuchProcess:
            pass
        except AttributeError:
            procstat['num_fds'] = None
        # Threads number
        try:
            procstat.update(proc.as_dict(attrs=['num_threads']))
        except psutil.NoSuchProcess:
            pass
        except AttributeError:
            procstat['num_threads'] = None

        # Number of handles (Windows only)
A
Alessio Sergi 已提交
316
        if WINDOWS:
317
            try:
N
nicolargo 已提交
318
                procstat.update(proc.as_dict(attrs=['num_handles']))
319 320
            except psutil.NoSuchProcess:
                pass
N
nicolargo 已提交
321 322 323 324 325
        else:
            procstat['num_handles'] = None

        # SWAP memory (Only on Linux based OS)
        # http://www.cyberciti.biz/faq/linux-which-process-is-using-swap/
A
Alessio Sergi 已提交
326
        if LINUX:
327
            try:
N
nicolargo 已提交
328 329
                procstat['memory_swap'] = sum(
                    [v.swap for v in proc.memory_maps()])
330 331
            except psutil.NoSuchProcess:
                pass
N
nicolargo 已提交
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347
            except psutil.AccessDenied:
                procstat['memory_swap'] = None
            except Exception:
                # Add a dirty except to handle the PsUtil issue #413
                procstat['memory_swap'] = None

        # Process network connections (TCP and UDP)
        try:
            procstat['tcp'] = len(proc.connections(kind="tcp"))
            procstat['udp'] = len(proc.connections(kind="udp"))
        except Exception:
            procstat['tcp'] = None
            procstat['udp'] = None

        # IO Nice
        # http://pythonhosted.org/psutil/#psutil.Process.ionice
A
Alessio Sergi 已提交
348
        if LINUX or WINDOWS:
349
            try:
N
nicolargo 已提交
350
                procstat.update(proc.as_dict(attrs=['ionice']))
351 352
            except psutil.NoSuchProcess:
                pass
N
nicolargo 已提交
353 354
        else:
            procstat['ionice'] = None
355

N
nicolargo 已提交
356
        return procstat
357

N
nicolargo 已提交
358 359 360 361
    def __get_process_stats(self, proc,
                            mandatory_stats=True,
                            standard_stats=True,
                            extended_stats=False):
A
PEP 257  
Alessio Sergi 已提交
362
        """Get stats of running processes."""
N
nicolargo 已提交
363 364 365 366
        # Process ID (always)
        procstat = proc.as_dict(attrs=['pid'])

        if mandatory_stats:
N
nicolargo 已提交
367
            procstat = self.__get_mandatory_stats(proc, procstat)
368

N
nicolargo 已提交
369
        if procstat is not None and standard_stats:
N
nicolargo 已提交
370
            procstat = self.__get_standard_stats(proc, procstat)
371

N
nicolargo 已提交
372
        if procstat is not None and extended_stats and not self.disable_extended_tag:
N
nicolargo 已提交
373
            procstat = self.__get_extended_stats(proc, procstat)
N
Nicolargo 已提交
374

A
Alessio Sergi 已提交
375 376 377
        return procstat

    def update(self):
A
PEP 257  
Alessio Sergi 已提交
378
        """Update the processes stats."""
379
        # Reset the stats
A
Alessio Sergi 已提交
380
        self.processlist = []
381
        self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0}
A
Alessio Sergi 已提交
382

383
        # Do not process if disable tag is set
384
        if self.disable_tag:
A
Alessio Sergi 已提交
385
            return
386

A
Alessio Sergi 已提交
387 388 389
        # Get the time since last update
        time_since_update = getTimeSinceLastUpdate('process_disk')

390 391
        # Build an internal dict with only mandatories stats (sort keys)
        processdict = {}
D
desbma 已提交
392
        excluded_processes = set()
A
Alessio Sergi 已提交
393
        for proc in psutil.process_iter():
394
            # Ignore kernel threads if needed
A
Alessio Sergi 已提交
395
            if self.no_kernel_threads and not WINDOWS and is_kernel_thread(proc):
396 397
                continue

398 399
            # If self.max_processes is None: Only retrieve mandatory stats
            # Else: retrieve mandatory and standard stats
400 401
            s = self.__get_process_stats(proc,
                                         mandatory_stats=True,
402
                                         standard_stats=self.max_processes is None)
403 404 405 406 407 408 409
            # ignore the 'idle' process on Windows and *BSD
            # ignore the 'kernel_task' process on OS X
            # waiting for upstream patch from psutil
            if (BSD and s['name'] == 'idle' or
               WINDOWS and s['name'] == 'System Idle Process' or
               OSX and s['name'] == 'kernel_task'):
                continue
N
Nicolargo 已提交
410
            # Continue to the next process if it has to be filtered
411
            if s is None or self._filter.is_filtered(s):
D
desbma 已提交
412
                excluded_processes.add(proc)
N
Nicolargo 已提交
413
                continue
414

N
Nicolargo 已提交
415
            # Ok add the process to the list
N
Nicolargo 已提交
416
            processdict[proc] = s
417 418 419 420 421
            # Update processcount (global statistics)
            try:
                self.processcount[str(proc.status())] += 1
            except KeyError:
                # Key did not exist, create it
422 423 424 425
                try:
                    self.processcount[str(proc.status())] = 1
                except psutil.NoSuchProcess:
                    pass
N
Nicolargo 已提交
426 427
            except psutil.NoSuchProcess:
                pass
428 429 430
            else:
                self.processcount['total'] += 1
            # Update thread number (global statistics)
A
Alessio Sergi 已提交
431
            try:
432
                self.processcount['thread'] += proc.num_threads()
A
Alessio Sergi 已提交
433
            except Exception:
434 435
                pass

436
        if self._enable_tree:
A
Alessio Sergi 已提交
437
            self.process_tree = ProcessTreeNode.build_tree(processdict,
438
                                                           self.sort_key,
A
Alessio Sergi 已提交
439
                                                           self.sort_reverse,
D
desbma 已提交
440 441
                                                           self.no_kernel_threads,
                                                           excluded_processes)
D
desbma 已提交
442

D
desbma 已提交
443
            for i, node in enumerate(self.process_tree):
444 445
                # Only retreive stats for visible processes (max_processes)
                if self.max_processes is not None and i >= self.max_processes:
446
                    break
D
desbma 已提交
447 448 449 450 451 452 453 454 455 456 457 458 459 460

                # add standard stats
                new_stats = self.__get_process_stats(node.process,
                                                     mandatory_stats=False,
                                                     standard_stats=True,
                                                     extended_stats=False)
                if new_stats is not None:
                    node.stats.update(new_stats)

                # Add a specific time_since_update stats for bitrate
                node.stats['time_since_update'] = time_since_update

        else:
            # Process optimization
461 462
            # Only retreive stats for visible processes (max_processes)
            if self.max_processes is not None:
D
desbma 已提交
463 464
                # Sort the internal dict and cut the top N (Return a list of tuple)
                # tuple=key (proc), dict (returned by __get_process_stats)
D
desbma 已提交
465
                try:
A
Alessio Sergi 已提交
466
                    processiter = sorted(iteritems(processdict),
A
Alessio Sergi 已提交
467 468
                                         key=lambda x: x[1][self.sort_key],
                                         reverse=self.sort_reverse)
469
                except (KeyError, TypeError) as e:
470 471
                    logger.error("Cannot sort process list by {}: {}".format(self.sort_key, e))
                    logger.error('{}'.format(listitems(processdict)[0]))
D
desbma 已提交
472
                    # Fallback to all process (issue #423)
A
Alessio Sergi 已提交
473
                    processloop = iteritems(processdict)
D
desbma 已提交
474 475
                    first = False
                else:
476
                    processloop = processiter[0:self.max_processes]
D
desbma 已提交
477
                    first = True
D
desbma 已提交
478 479
            else:
                # Get all processes stats
A
Alessio Sergi 已提交
480
                processloop = iteritems(processdict)
D
desbma 已提交
481
                first = False
482

D
desbma 已提交
483 484 485
            for i in processloop:
                # Already existing mandatory stats
                procstat = i[1]
486
                if self.max_processes is not None:
D
desbma 已提交
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501
                    # Update with standard stats
                    # and extended stats but only for TOP (first) process
                    s = self.__get_process_stats(i[0],
                                                 mandatory_stats=False,
                                                 standard_stats=True,
                                                 extended_stats=first)
                    if s is None:
                        continue
                    procstat.update(s)
                # Add a specific time_since_update stats for bitrate
                procstat['time_since_update'] = time_since_update
                # Update process list
                self.processlist.append(procstat)
                # Next...
                first = False
A
Alessio Sergi 已提交
502

503
        # Build the all processes list used by the AMPs
N
nicolargo 已提交
504
        self.allprocesslist = [p for p in itervalues(processdict)]
505

A
Alessio Sergi 已提交
506
        # Clean internals caches if timeout is reached
507
        if self.cache_timer.finished():
A
Alessio Sergi 已提交
508 509 510 511 512 513
            self.username_cache = {}
            self.cmdline_cache = {}
            # Restart the timer
            self.cache_timer.reset()

    def getcount(self):
A
PEP 257  
Alessio Sergi 已提交
514
        """Get the number of processes."""
A
Alessio Sergi 已提交
515 516
        return self.processcount

517 518 519 520
    def getalllist(self):
        """Get the allprocesslist."""
        return self.allprocesslist

A
Alessio Sergi 已提交
521
    def getlist(self, sortedby=None):
A
PEP 257  
Alessio Sergi 已提交
522
        """Get the processlist."""
523 524
        return self.processlist

D
desbma 已提交
525 526 527 528
    def gettree(self):
        """Get the process tree."""
        return self.process_tree

529 530 531 532 533 534 535 536 537
    @property
    def sort_key(self):
        """Get the current sort key."""
        return self._sort_key

    @sort_key.setter
    def sort_key(self, key):
        """Set the current sort key."""
        self._sort_key = key
A
Alessio Sergi 已提交
538

539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575

# TODO: move this global function (also used in glances_processlist
#       and logs) inside the GlancesProcesses class
def sort_stats(stats, sortedby=None, tree=False, reverse=True):
    """Return the stats (dict) sorted by (sortedby)
    Reverse the sort if reverse is True."""
    if sortedby is None:
        # No need to sort...
        return stats

    if sortedby == 'io_counters' and not tree:
        # Specific case for io_counters
        # Sum of io_r + io_w
        try:
            # Sort process by IO rate (sum IO read + IO write)
            stats.sort(key=lambda process: process[sortedby][0] -
                       process[sortedby][2] + process[sortedby][1] -
                       process[sortedby][3],
                       reverse=reverse)
        except Exception:
            stats.sort(key=operator.itemgetter('cpu_percent'),
                       reverse=reverse)
    else:
        # Others sorts
        if tree:
            stats.set_sorting(sortedby, reverse)
        else:
            try:
                stats.sort(key=operator.itemgetter(sortedby),
                           reverse=reverse)
            except (KeyError, TypeError):
                stats.sort(key=operator.itemgetter('name'),
                           reverse=False)

    return stats


A
Alessio Sergi 已提交
576
glances_processes = GlancesProcesses()