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