You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

416 lines
15 KiB

4 years ago
  1. # -*- coding: utf-8 -*-
  2. #
  3. # This file is part of Glances.
  4. #
  5. # Copyright (C) 2018 Nicolargo <nicolas@nicolargo.com>
  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. import operator
  20. import os
  21. from glances.compat import iteritems, itervalues, listitems, iterkeys
  22. from glances.globals import BSD, LINUX, MACOS, SUNOS, WINDOWS
  23. from glances.timer import Timer, getTimeSinceLastUpdate
  24. from glances.filter import GlancesFilter
  25. from glances.logger import logger
  26. import psutil
  27. class GlancesProcesses(object):
  28. """Get processed stats using the psutil library."""
  29. def __init__(self, cache_timeout=60):
  30. """Init the class to collect stats about processes."""
  31. # Add internals caches because psutil do not cache all the stats
  32. # See: https://code.google.com/p/psutil/issues/detail?id=462
  33. self.username_cache = {}
  34. self.cmdline_cache = {}
  35. # The internals caches will be cleaned each 'cache_timeout' seconds
  36. self.cache_timeout = cache_timeout
  37. self.cache_timer = Timer(self.cache_timeout)
  38. # Init the io dict
  39. # key = pid
  40. # value = [ read_bytes_old, write_bytes_old ]
  41. self.io_old = {}
  42. # Init stats
  43. self.auto_sort = True
  44. self._sort_key = 'cpu_percent'
  45. self.processlist = []
  46. self.reset_processcount()
  47. # Tag to enable/disable the processes stats (to reduce the Glances CPU consumption)
  48. # Default is to enable the processes stats
  49. self.disable_tag = False
  50. # Extended stats for top process is enable by default
  51. self.disable_extended_tag = False
  52. # Maximum number of processes showed in the UI (None if no limit)
  53. self._max_processes = None
  54. # Process filter is a regular expression
  55. self._filter = GlancesFilter()
  56. # Whether or not to hide kernel threads
  57. self.no_kernel_threads = False
  58. # Store maximums values in a dict
  59. # Used in the UI to highlight the maximum value
  60. self._max_values_list = ('cpu_percent', 'memory_percent')
  61. # { 'cpu_percent': 0.0, 'memory_percent': 0.0 }
  62. self._max_values = {}
  63. self.reset_max_values()
  64. def reset_processcount(self):
  65. """Reset the global process count"""
  66. self.processcount = {'total': 0,
  67. 'running': 0,
  68. 'sleeping': 0,
  69. 'thread': 0,
  70. 'pid_max': None}
  71. def update_processcount(self, plist):
  72. """Update the global process count from the current processes list"""
  73. # Update the maximum process ID (pid) number
  74. self.processcount['pid_max'] = self.pid_max
  75. # For each key in the processcount dict
  76. # count the number of processes with the same status
  77. for k in iterkeys(self.processcount):
  78. self.processcount[k] = len(list(filter(lambda v: v['status'] is k,
  79. plist)))
  80. # Compute thread
  81. self.processcount['thread'] = sum(i['num_threads'] for i in plist
  82. if i['num_threads'] is not None)
  83. # Compute total
  84. self.processcount['total'] = len(plist)
  85. def enable(self):
  86. """Enable process stats."""
  87. self.disable_tag = False
  88. self.update()
  89. def disable(self):
  90. """Disable process stats."""
  91. self.disable_tag = True
  92. def enable_extended(self):
  93. """Enable extended process stats."""
  94. self.disable_extended_tag = False
  95. self.update()
  96. def disable_extended(self):
  97. """Disable extended process stats."""
  98. self.disable_extended_tag = True
  99. @property
  100. def pid_max(self):
  101. """
  102. Get the maximum PID value.
  103. On Linux, the value is read from the `/proc/sys/kernel/pid_max` file.
  104. From `man 5 proc`:
  105. The default value for this file, 32768, results in the same range of
  106. PIDs as on earlier kernels. On 32-bit platfroms, 32768 is the maximum
  107. value for pid_max. On 64-bit systems, pid_max can be set to any value
  108. up to 2^22 (PID_MAX_LIMIT, approximately 4 million).
  109. If the file is unreadable or not available for whatever reason,
  110. returns None.
  111. Some other OSes:
  112. - On FreeBSD and macOS the maximum is 99999.
  113. - On OpenBSD >= 6.0 the maximum is 99999 (was 32766).
  114. - On NetBSD the maximum is 30000.
  115. :returns: int or None
  116. """
  117. if LINUX:
  118. # XXX: waiting for https://github.com/giampaolo/psutil/issues/720
  119. try:
  120. with open('/proc/sys/kernel/pid_max', 'rb') as f:
  121. return int(f.read())
  122. except (OSError, IOError):
  123. return None
  124. else:
  125. return None
  126. @property
  127. def max_processes(self):
  128. """Get the maximum number of processes showed in the UI."""
  129. return self._max_processes
  130. @max_processes.setter
  131. def max_processes(self, value):
  132. """Set the maximum number of processes showed in the UI."""
  133. self._max_processes = value
  134. @property
  135. def process_filter_input(self):
  136. """Get the process filter (given by the user)."""
  137. return self._filter.filter_input
  138. @property
  139. def process_filter(self):
  140. """Get the process filter (current apply filter)."""
  141. return self._filter.filter
  142. @process_filter.setter
  143. def process_filter(self, value):
  144. """Set the process filter."""
  145. self._filter.filter = value
  146. @property
  147. def process_filter_key(self):
  148. """Get the process filter key."""
  149. return self._filter.filter_key
  150. @property
  151. def process_filter_re(self):
  152. """Get the process regular expression compiled."""
  153. return self._filter.filter_re
  154. def disable_kernel_threads(self):
  155. """Ignore kernel threads in process list."""
  156. self.no_kernel_threads = True
  157. @property
  158. def sort_reverse(self):
  159. """Return True to sort processes in reverse 'key' order, False instead."""
  160. if self.sort_key == 'name' or self.sort_key == 'username':
  161. return False
  162. return True
  163. def max_values(self):
  164. """Return the max values dict."""
  165. return self._max_values
  166. def get_max_values(self, key):
  167. """Get the maximum values of the given stat (key)."""
  168. return self._max_values[key]
  169. def set_max_values(self, key, value):
  170. """Set the maximum value for a specific stat (key)."""
  171. self._max_values[key] = value
  172. def reset_max_values(self):
  173. """Reset the maximum values dict."""
  174. self._max_values = {}
  175. for k in self._max_values_list:
  176. self._max_values[k] = 0.0
  177. def update(self):
  178. """Update the processes stats."""
  179. # Reset the stats
  180. self.processlist = []
  181. self.reset_processcount()
  182. # Do not process if disable tag is set
  183. if self.disable_tag:
  184. return
  185. # Time since last update (for disk_io rate computation)
  186. time_since_update = getTimeSinceLastUpdate('process_disk')
  187. # Grab standard stats
  188. #####################
  189. standard_attrs = ['cmdline', 'cpu_percent', 'cpu_times', 'memory_info',
  190. 'memory_percent', 'name', 'nice', 'pid', 'ppid',
  191. 'status', 'username', 'status', 'num_threads']
  192. # io_counters availability: Linux, BSD, Windows, AIX
  193. if not MACOS and not SUNOS:
  194. standard_attrs += ['io_counters']
  195. # gids availability: Unix
  196. if not WINDOWS:
  197. standard_attrs += ['gids']
  198. # and build the processes stats list (psutil>=5.3.0)
  199. self.processlist = [p.info for p in psutil.process_iter(attrs=standard_attrs,
  200. ad_value=None)
  201. # OS-related processes filter
  202. if not (BSD and p.info['name'] == 'idle') and
  203. not (WINDOWS and p.info['name'] == 'System Idle Process') and
  204. not (MACOS and p.info['name'] == 'kernel_task') and
  205. # Kernel threads filter
  206. not (self.no_kernel_threads and LINUX and p.info['gids'].real == 0) and
  207. # User filter
  208. not (self._filter.is_filtered(p.info))]
  209. # Sort the processes list by the current sort_key
  210. self.processlist = sort_stats(self.processlist,
  211. sortedby=self.sort_key,
  212. reverse=True)
  213. # Update the processcount
  214. self.update_processcount(self.processlist)
  215. # Loop over processes and add metadata
  216. first = True
  217. for proc in self.processlist:
  218. # Get extended stats, only for top processes (see issue #403).
  219. if first and not self.disable_extended_tag:
  220. # - cpu_affinity (Linux, Windows, FreeBSD)
  221. # - ionice (Linux and Windows > Vista)
  222. # - num_ctx_switches (not available on Illumos/Solaris)
  223. # - num_fds (Unix-like)
  224. # - num_handles (Windows)
  225. # - memory_maps (only swap, Linux)
  226. # https://www.cyberciti.biz/faq/linux-which-process-is-using-swap/
  227. # - connections (TCP and UDP)
  228. extended = {}
  229. try:
  230. top_process = psutil.Process(proc['pid'])
  231. extended_stats = ['cpu_affinity', 'ionice',
  232. 'num_ctx_switches', 'num_fds']
  233. if WINDOWS:
  234. extended_stats += ['num_handles']
  235. # Get the extended stats
  236. extended = top_process.as_dict(attrs=extended_stats,
  237. ad_value=None)
  238. if LINUX:
  239. try:
  240. extended['memory_swap'] = sum([v.swap for v in top_process.memory_maps()])
  241. except psutil.NoSuchProcess:
  242. pass
  243. except (psutil.AccessDenied, NotImplementedError):
  244. # NotImplementedError: /proc/${PID}/smaps file doesn't exist
  245. # on kernel < 2.6.14 or CONFIG_MMU kernel configuration option
  246. # is not enabled (see psutil #533/glances #413).
  247. extended['memory_swap'] = None
  248. try:
  249. extended['tcp'] = len(top_process.connections(kind="tcp"))
  250. extended['udp'] = len(top_process.connections(kind="udp"))
  251. except (psutil.AccessDenied, psutil.NoSuchProcess):
  252. # Manage issue1283 (psutil.AccessDenied)
  253. extended['tcp'] = None
  254. extended['udp'] = None
  255. except (psutil.NoSuchProcess, ValueError, AttributeError) as e:
  256. logger.error('Can not grab extended stats ({})'.format(e))
  257. extended['extended_stats'] = False
  258. else:
  259. logger.debug('Grab extended stats for process {}'.format(proc['pid']))
  260. extended['extended_stats'] = True
  261. proc.update(extended)
  262. first = False
  263. # /End of extended stats
  264. # Time since last update (for disk_io rate computation)
  265. proc['time_since_update'] = time_since_update
  266. # Process status (only keep the first char)
  267. proc['status'] = str(proc['status'])[:1].upper()
  268. # Process IO
  269. # procstat['io_counters'] is a list:
  270. # [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag]
  271. # If io_tag = 0 > Access denied or first time (display "?")
  272. # If io_tag = 1 > No access denied (display the IO rate)
  273. if 'io_counters' in proc and proc['io_counters'] is not None:
  274. io_new = [proc['io_counters'].read_bytes,
  275. proc['io_counters'].write_bytes]
  276. # For IO rate computation
  277. # Append saved IO r/w bytes
  278. try:
  279. proc['io_counters'] = io_new + self.io_old[proc['pid']]
  280. io_tag = 1
  281. except KeyError:
  282. proc['io_counters'] = io_new + [0, 0]
  283. io_tag = 0
  284. # then save the IO r/w bytes
  285. self.io_old[proc['pid']] = io_new
  286. else:
  287. proc['io_counters'] = [0, 0] + [0, 0]
  288. io_tag = 0
  289. # Append the IO tag (for display)
  290. proc['io_counters'] += [io_tag]
  291. # Compute the maximum value for keys in self._max_values_list: CPU, MEM
  292. # Usefull to highlight the processes with maximum values
  293. for k in self._max_values_list:
  294. values_list = [i[k] for i in self.processlist if i[k] is not None]
  295. if values_list != []:
  296. self.set_max_values(k, max(values_list))
  297. def getcount(self):
  298. """Get the number of processes."""
  299. return self.processcount
  300. def getlist(self, sortedby=None):
  301. """Get the processlist."""
  302. return self.processlist
  303. @property
  304. def sort_key(self):
  305. """Get the current sort key."""
  306. return self._sort_key
  307. @sort_key.setter
  308. def sort_key(self, key):
  309. """Set the current sort key."""
  310. self._sort_key = key
  311. def weighted(value):
  312. """Manage None value in dict value."""
  313. return -float('inf') if value is None else value
  314. def sort_stats(stats, sortedby=None, reverse=True):
  315. """Return the stats (dict) sorted by (sortedby).
  316. Reverse the sort if reverse is True.
  317. """
  318. sortedby_secondary = 'cpu_percent'
  319. if sortedby is None:
  320. # No need to sort...
  321. return stats
  322. elif sortedby is 'cpu_percent':
  323. sortedby_secondary = 'memory_percent'
  324. if sortedby == 'io_counters':
  325. # Specific case for io_counters
  326. # Sum of io_r + io_w
  327. try:
  328. # Sort process by IO rate (sum IO read + IO write)
  329. stats.sort(key=lambda process: process[sortedby][0] -
  330. process[sortedby][2] + process[sortedby][1] -
  331. process[sortedby][3],
  332. reverse=reverse)
  333. except Exception:
  334. stats.sort(key=lambda x: (weighted(x['cpu_percent']),
  335. weighted(x['memory_percent'])),
  336. reverse=reverse)
  337. else:
  338. # Others sorts
  339. try:
  340. stats.sort(key=lambda x: (weighted(x[sortedby]),
  341. weighted(x[sortedby_secondary])),
  342. reverse=reverse)
  343. except (KeyError, TypeError):
  344. stats.sort(key=operator.itemgetter('name'),
  345. reverse=False)
  346. return stats
  347. glances_processes = GlancesProcesses()