|
|
- # -*- coding: utf-8 -*-
- #
- # This file is part of Glances.
- #
- # Copyright (C) 2018 Nicolargo <nicolas@nicolargo.com>
- #
- # 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/>.
-
- import operator
- import os
-
- from glances.compat import iteritems, itervalues, listitems, iterkeys
- from glances.globals import BSD, LINUX, MACOS, SUNOS, WINDOWS
- from glances.timer import Timer, getTimeSinceLastUpdate
- from glances.filter import GlancesFilter
- from glances.logger import logger
-
- import psutil
-
-
- class GlancesProcesses(object):
-
- """Get processed stats using the psutil library."""
-
- def __init__(self, cache_timeout=60):
- """Init the class to collect stats about processes."""
- # 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 = {}
-
- # The internals caches will be cleaned each 'cache_timeout' seconds
- self.cache_timeout = cache_timeout
- self.cache_timer = Timer(self.cache_timeout)
-
- # Init the io dict
- # key = pid
- # value = [ read_bytes_old, write_bytes_old ]
- self.io_old = {}
-
- # Init stats
- self.auto_sort = True
- self._sort_key = 'cpu_percent'
- self.processlist = []
- self.reset_processcount()
-
- # Tag to enable/disable the processes stats (to reduce the Glances CPU consumption)
- # Default is to enable the processes stats
- self.disable_tag = False
-
- # Extended stats for top process is enable by default
- self.disable_extended_tag = False
-
- # Maximum number of processes showed in the UI (None if no limit)
- self._max_processes = None
-
- # Process filter is a regular expression
- self._filter = GlancesFilter()
-
- # Whether or not to hide kernel threads
- self.no_kernel_threads = False
-
- # Store maximums values in a dict
- # Used in the UI to highlight the maximum value
- self._max_values_list = ('cpu_percent', 'memory_percent')
- # { 'cpu_percent': 0.0, 'memory_percent': 0.0 }
- self._max_values = {}
- self.reset_max_values()
-
- def reset_processcount(self):
- """Reset the global process count"""
- self.processcount = {'total': 0,
- 'running': 0,
- 'sleeping': 0,
- 'thread': 0,
- 'pid_max': None}
-
- def update_processcount(self, plist):
- """Update the global process count from the current processes list"""
- # Update the maximum process ID (pid) number
- self.processcount['pid_max'] = self.pid_max
- # For each key in the processcount dict
- # count the number of processes with the same status
- for k in iterkeys(self.processcount):
- self.processcount[k] = len(list(filter(lambda v: v['status'] is k,
- plist)))
- # Compute thread
- self.processcount['thread'] = sum(i['num_threads'] for i in plist
- if i['num_threads'] is not None)
- # Compute total
- self.processcount['total'] = len(plist)
-
- def enable(self):
- """Enable process stats."""
- self.disable_tag = False
- self.update()
-
- def disable(self):
- """Disable process stats."""
- self.disable_tag = True
-
- 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
-
- @property
- def pid_max(self):
- """
- Get the maximum PID value.
-
- On Linux, the value is read from the `/proc/sys/kernel/pid_max` file.
-
- From `man 5 proc`:
- The default value for this file, 32768, results in the same range of
- PIDs as on earlier kernels. On 32-bit platfroms, 32768 is the maximum
- value for pid_max. On 64-bit systems, pid_max can be set to any value
- up to 2^22 (PID_MAX_LIMIT, approximately 4 million).
-
- If the file is unreadable or not available for whatever reason,
- returns None.
-
- Some other OSes:
- - On FreeBSD and macOS the maximum is 99999.
- - On OpenBSD >= 6.0 the maximum is 99999 (was 32766).
- - On NetBSD the maximum is 30000.
-
- :returns: int or None
- """
- if LINUX:
- # XXX: waiting for https://github.com/giampaolo/psutil/issues/720
- try:
- with open('/proc/sys/kernel/pid_max', 'rb') as f:
- return int(f.read())
- except (OSError, IOError):
- return None
- else:
- return None
-
- @property
- def max_processes(self):
- """Get the maximum number of processes showed in the UI."""
- return self._max_processes
-
- @max_processes.setter
- def max_processes(self, value):
- """Set the maximum number of processes showed in the UI."""
- self._max_processes = value
-
- @property
- def process_filter_input(self):
- """Get the process filter (given by the user)."""
- return self._filter.filter_input
-
- @property
- def process_filter(self):
- """Get the process filter (current apply filter)."""
- return self._filter.filter
-
- @process_filter.setter
- def process_filter(self, value):
- """Set the process filter."""
- self._filter.filter = value
-
- @property
- def process_filter_key(self):
- """Get the process filter key."""
- return self._filter.filter_key
-
- @property
- def process_filter_re(self):
- """Get the process regular expression compiled."""
- return self._filter.filter_re
-
- def disable_kernel_threads(self):
- """Ignore kernel threads in process list."""
- self.no_kernel_threads = True
-
- @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
-
- def max_values(self):
- """Return the max values dict."""
- return self._max_values
-
- def get_max_values(self, key):
- """Get the maximum values of the given stat (key)."""
- return self._max_values[key]
-
- def set_max_values(self, key, value):
- """Set the maximum value for a specific stat (key)."""
- self._max_values[key] = value
-
- def reset_max_values(self):
- """Reset the maximum values dict."""
- self._max_values = {}
- for k in self._max_values_list:
- self._max_values[k] = 0.0
-
- def update(self):
- """Update the processes stats."""
- # Reset the stats
- self.processlist = []
- self.reset_processcount()
-
- # Do not process if disable tag is set
- if self.disable_tag:
- return
-
- # Time since last update (for disk_io rate computation)
- time_since_update = getTimeSinceLastUpdate('process_disk')
-
- # Grab standard stats
- #####################
- standard_attrs = ['cmdline', 'cpu_percent', 'cpu_times', 'memory_info',
- 'memory_percent', 'name', 'nice', 'pid', 'ppid',
- 'status', 'username', 'status', 'num_threads']
- # io_counters availability: Linux, BSD, Windows, AIX
- if not MACOS and not SUNOS:
- standard_attrs += ['io_counters']
- # gids availability: Unix
- if not WINDOWS:
- standard_attrs += ['gids']
-
- # and build the processes stats list (psutil>=5.3.0)
- self.processlist = [p.info for p in psutil.process_iter(attrs=standard_attrs,
- ad_value=None)
- # OS-related processes filter
- if not (BSD and p.info['name'] == 'idle') and
- not (WINDOWS and p.info['name'] == 'System Idle Process') and
- not (MACOS and p.info['name'] == 'kernel_task') and
- # Kernel threads filter
- not (self.no_kernel_threads and LINUX and p.info['gids'].real == 0) and
- # User filter
- not (self._filter.is_filtered(p.info))]
-
- # Sort the processes list by the current sort_key
- self.processlist = sort_stats(self.processlist,
- sortedby=self.sort_key,
- reverse=True)
-
- # Update the processcount
- self.update_processcount(self.processlist)
-
- # Loop over processes and add metadata
- first = True
- for proc in self.processlist:
- # Get extended stats, only for top processes (see issue #403).
- if first and not self.disable_extended_tag:
- # - cpu_affinity (Linux, Windows, FreeBSD)
- # - ionice (Linux and Windows > Vista)
- # - num_ctx_switches (not available on Illumos/Solaris)
- # - num_fds (Unix-like)
- # - num_handles (Windows)
- # - memory_maps (only swap, Linux)
- # https://www.cyberciti.biz/faq/linux-which-process-is-using-swap/
- # - connections (TCP and UDP)
- extended = {}
- try:
- top_process = psutil.Process(proc['pid'])
- extended_stats = ['cpu_affinity', 'ionice',
- 'num_ctx_switches', 'num_fds']
- if WINDOWS:
- extended_stats += ['num_handles']
-
- # Get the extended stats
- extended = top_process.as_dict(attrs=extended_stats,
- ad_value=None)
-
- if LINUX:
- try:
- extended['memory_swap'] = sum([v.swap for v in top_process.memory_maps()])
- except psutil.NoSuchProcess:
- pass
- except (psutil.AccessDenied, NotImplementedError):
- # NotImplementedError: /proc/${PID}/smaps file doesn't exist
- # on kernel < 2.6.14 or CONFIG_MMU kernel configuration option
- # is not enabled (see psutil #533/glances #413).
- extended['memory_swap'] = None
- try:
- extended['tcp'] = len(top_process.connections(kind="tcp"))
- extended['udp'] = len(top_process.connections(kind="udp"))
- except (psutil.AccessDenied, psutil.NoSuchProcess):
- # Manage issue1283 (psutil.AccessDenied)
- extended['tcp'] = None
- extended['udp'] = None
- except (psutil.NoSuchProcess, ValueError, AttributeError) as e:
- logger.error('Can not grab extended stats ({})'.format(e))
- extended['extended_stats'] = False
- else:
- logger.debug('Grab extended stats for process {}'.format(proc['pid']))
- extended['extended_stats'] = True
- proc.update(extended)
- first = False
- # /End of extended stats
-
- # Time since last update (for disk_io rate computation)
- proc['time_since_update'] = time_since_update
-
- # Process status (only keep the first char)
- proc['status'] = str(proc['status'])[:1].upper()
-
- # 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 or first time (display "?")
- # If io_tag = 1 > No access denied (display the IO rate)
- if 'io_counters' in proc and proc['io_counters'] is not None:
- io_new = [proc['io_counters'].read_bytes,
- proc['io_counters'].write_bytes]
- # For IO rate computation
- # Append saved IO r/w bytes
- try:
- proc['io_counters'] = io_new + self.io_old[proc['pid']]
- io_tag = 1
- except KeyError:
- proc['io_counters'] = io_new + [0, 0]
- io_tag = 0
- # then save the IO r/w bytes
- self.io_old[proc['pid']] = io_new
- else:
- proc['io_counters'] = [0, 0] + [0, 0]
- io_tag = 0
- # Append the IO tag (for display)
- proc['io_counters'] += [io_tag]
-
- # Compute the maximum value for keys in self._max_values_list: CPU, MEM
- # Usefull to highlight the processes with maximum values
- for k in self._max_values_list:
- values_list = [i[k] for i in self.processlist if i[k] is not None]
- if values_list != []:
- self.set_max_values(k, max(values_list))
-
- def getcount(self):
- """Get the number of processes."""
- return self.processcount
-
- def getlist(self, sortedby=None):
- """Get the processlist."""
- return self.processlist
-
- @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
-
-
- def weighted(value):
- """Manage None value in dict value."""
- return -float('inf') if value is None else value
-
-
- def sort_stats(stats, sortedby=None, reverse=True):
- """Return the stats (dict) sorted by (sortedby).
-
- Reverse the sort if reverse is True.
- """
- sortedby_secondary = 'cpu_percent'
- if sortedby is None:
- # No need to sort...
- return stats
- elif sortedby is 'cpu_percent':
- sortedby_secondary = 'memory_percent'
-
- if sortedby == 'io_counters':
- # 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=lambda x: (weighted(x['cpu_percent']),
- weighted(x['memory_percent'])),
- reverse=reverse)
- else:
- # Others sorts
- try:
- stats.sort(key=lambda x: (weighted(x[sortedby]),
- weighted(x[sortedby_secondary])),
- reverse=reverse)
- except (KeyError, TypeError):
- stats.sort(key=operator.itemgetter('name'),
- reverse=False)
-
- return stats
-
-
- glances_processes = GlancesProcesses()
|