Issues (46)

glances/processes.py (1 issue)

Severity
1
# -*- coding: utf-8 -*-
2
#
3
# This file is part of Glances.
4
#
5
# SPDX-FileCopyrightText: 2023 Nicolas Hennion <[email protected]>
6
#
7
# SPDX-License-Identifier: LGPL-3.0-only
8
#
9
10
import os
11
12
from glances.compat import iterkeys
13
from glances.globals import BSD, LINUX, MACOS, WINDOWS
14
from glances.timer import Timer, getTimeSinceLastUpdate
15
from glances.filter import GlancesFilter
16
from glances.programs import processes_to_programs
17
from glances.logger import logger
18
19
import psutil
20
21
# This constant defines the list of available processes sort key
22
sort_processes_key_list = ['cpu_percent', 'memory_percent', 'username', 'cpu_times', 'io_counters', 'name']
23
24
# Sort dictionary for human
25
sort_for_human = {
26
    'io_counters': 'disk IO',
27
    'cpu_percent': 'CPU consumption',
28
    'memory_percent': 'memory consumption',
29
    'cpu_times': 'process time',
30
    'username': 'user name',
31
    'name': 'processs name',
32
    None: 'None',
33
}
34
35
36
class GlancesProcesses(object):
37
    """Get processed stats using the psutil library."""
38
39
    def __init__(self, cache_timeout=60):
40
        """Init the class to collect stats about processes."""
41
        # Init the args, coming from the GlancesStandalone class
42
        # Should be set by the set_args method
43
        self.args = None
44
45
        # Add internals caches because psutil do not cache all the stats
46
        # See: https://github.com/giampaolo/psutil/issues/462
47
        self.username_cache = {}
48
        self.cmdline_cache = {}
49
50
        # The internals caches will be cleaned each 'cache_timeout' seconds
51
        self.cache_timeout = cache_timeout
52
        # First iteration, no cache
53
        self.cache_timer = Timer(0)
54
55
        # Init the io_old dict used to compute the IO bitrate
56
        # key = pid
57
        # value = [ read_bytes_old, write_bytes_old ]
58
        self.io_old = {}
59
60
        # Init stats
61
        self.auto_sort = None
62
        self._sort_key = None
63
        # Default processes sort key is 'auto'
64
        # Can be overwrite from the configuration file (issue#1536) => See glances_processlist.py init
65
        self.set_sort_key('auto', auto=True)
66
        self.processlist = []
67
        self.reset_processcount()
68
69
        # Cache is a dict with key=pid and value = dict of cached value
70
        self.processlist_cache = {}
71
72
        # Tag to enable/disable the processes stats (to reduce the Glances CPU consumption)
73
        # Default is to enable the processes stats
74
        self.disable_tag = False
75
76
        # Extended stats for top process is enable by default
77
        self.disable_extended_tag = False
78
        self.extended_process = None
79
80
        # Test if the system can grab io_counters
81
        try:
82
            p = psutil.Process()
83
            p.io_counters()
84
        except Exception as e:
85
            logger.warning('PsUtil can not grab processes io_counters ({})'.format(e))
86
            self.disable_io_counters = True
87
        else:
88
            logger.debug('PsUtil can grab processes io_counters')
89
            self.disable_io_counters = False
90
91
        # Test if the system can grab gids
92
        try:
93
            p = psutil.Process()
94
            p.gids()
95
        except Exception as e:
96
            logger.warning('PsUtil can not grab processes gids ({})'.format(e))
97
            self.disable_gids = True
98
        else:
99
            logger.debug('PsUtil can grab processes gids')
100
            self.disable_gids = False
101
102
        # Maximum number of processes showed in the UI (None if no limit)
103
        self._max_processes = None
104
105
        # Process filter is a regular expression
106
        self._filter = GlancesFilter()
107
108
        # Whether or not to hide kernel threads
109
        self.no_kernel_threads = False
110
111
        # Store maximums values in a dict
112
        # Used in the UI to highlight the maximum value
113
        self._max_values_list = ('cpu_percent', 'memory_percent')
114
        # { 'cpu_percent': 0.0, 'memory_percent': 0.0 }
115
        self._max_values = {}
116
        self.reset_max_values()
117
118
    def set_args(self, args):
119
        """Set args."""
120
        self.args = args
121
122
    def reset_processcount(self):
123
        """Reset the global process count"""
124
        self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0, 'pid_max': None}
125
126
    def update_processcount(self, plist):
127
        """Update the global process count from the current processes list"""
128
        # Update the maximum process ID (pid) number
129
        self.processcount['pid_max'] = self.pid_max
130
        # For each key in the processcount dict
131
        # count the number of processes with the same status
132
        for k in iterkeys(self.processcount):
133
            self.processcount[k] = len(list(filter(lambda v: v['status'] is k, plist)))
0 ignored issues
show
The variable k does not seem to be defined in case the for loop on line 132 is not entered. Are you sure this can never be the case?
Loading history...
134
        # Compute thread
135
        self.processcount['thread'] = sum(i['num_threads'] for i in plist if i['num_threads'] is not None)
136
        # Compute total
137
        self.processcount['total'] = len(plist)
138
139
    def enable(self):
140
        """Enable process stats."""
141
        self.disable_tag = False
142
        self.update()
143
144
    def disable(self):
145
        """Disable process stats."""
146
        self.disable_tag = True
147
148
    def enable_extended(self):
149
        """Enable extended process stats."""
150
        self.disable_extended_tag = False
151
        self.update()
152
153
    def disable_extended(self):
154
        """Disable extended process stats."""
155
        self.disable_extended_tag = True
156
157
    @property
158
    def pid_max(self):
159
        """
160
        Get the maximum PID value.
161
162
        On Linux, the value is read from the `/proc/sys/kernel/pid_max` file.
163
164
        From `man 5 proc`:
165
        The default value for this file, 32768, results in the same range of
166
        PIDs as on earlier kernels. On 32-bit platforms, 32768 is the maximum
167
        value for pid_max. On 64-bit systems, pid_max can be set to any value
168
        up to 2^22 (PID_MAX_LIMIT, approximately 4 million).
169
170
        If the file is unreadable or not available for whatever reason,
171
        returns None.
172
173
        Some other OSes:
174
        - On FreeBSD and macOS the maximum is 99999.
175
        - On OpenBSD >= 6.0 the maximum is 99999 (was 32766).
176
        - On NetBSD the maximum is 30000.
177
178
        :returns: int or None
179
        """
180
        if LINUX:
181
            # XXX: waiting for https://github.com/giampaolo/psutil/issues/720
182
            try:
183
                with open('/proc/sys/kernel/pid_max', 'rb') as f:
184
                    return int(f.read())
185
            except (OSError, IOError):
186
                return None
187
        else:
188
            return None
189
190
    @property
191
    def processes_count(self):
192
        """Get the current number of processes showed in the UI."""
193
        return min(self._max_processes - 2, glances_processes.processcount['total'] - 1)
194
195
    @property
196
    def max_processes(self):
197
        """Get the maximum number of processes showed in the UI."""
198
        return self._max_processes
199
200
    @max_processes.setter
201
    def max_processes(self, value):
202
        """Set the maximum number of processes showed in the UI."""
203
        self._max_processes = value
204
205
    @property
206
    def process_filter_input(self):
207
        """Get the process filter (given by the user)."""
208
        return self._filter.filter_input
209
210
    @property
211
    def process_filter(self):
212
        """Get the process filter (current apply filter)."""
213
        return self._filter.filter
214
215
    @process_filter.setter
216
    def process_filter(self, value):
217
        """Set the process filter."""
218
        self._filter.filter = value
219
220
    @property
221
    def process_filter_key(self):
222
        """Get the process filter key."""
223
        return self._filter.filter_key
224
225
    @property
226
    def process_filter_re(self):
227
        """Get the process regular expression compiled."""
228
        return self._filter.filter_re
229
230
    def disable_kernel_threads(self):
231
        """Ignore kernel threads in process list."""
232
        self.no_kernel_threads = True
233
234
    @property
235
    def sort_reverse(self):
236
        """Return True to sort processes in reverse 'key' order, False instead."""
237
        if self.sort_key == 'name' or self.sort_key == 'username':
238
            return False
239
240
        return True
241
242
    def max_values(self):
243
        """Return the max values dict."""
244
        return self._max_values
245
246
    def get_max_values(self, key):
247
        """Get the maximum values of the given stat (key)."""
248
        return self._max_values[key]
249
250
    def set_max_values(self, key, value):
251
        """Set the maximum value for a specific stat (key)."""
252
        self._max_values[key] = value
253
254
    def reset_max_values(self):
255
        """Reset the maximum values dict."""
256
        self._max_values = {}
257
        for k in self._max_values_list:
258
            self._max_values[k] = 0.0
259
260
    def get_extended_stats(self, proc):
261
        """Get the extended stats for the given PID."""
262
        # - cpu_affinity (Linux, Windows, FreeBSD)
263
        # - ionice (Linux and Windows > Vista)
264
        # - num_ctx_switches (not available on Illumos/Solaris)
265
        # - num_fds (Unix-like)
266
        # - num_handles (Windows)
267
        # - memory_maps (only swap, Linux)
268
        #   https://www.cyberciti.biz/faq/linux-which-process-is-using-swap/
269
        # - connections (TCP and UDP)
270
        # - CPU min/max/mean
271
272
        # Set the extended stats list (OS dependant)
273
        extended_stats = ['cpu_affinity', 'ionice', 'num_ctx_switches']
274
        if LINUX:
275
            # num_fds only available on Unix system (see issue #1351)
276
            extended_stats += ['num_fds']
277
        if WINDOWS:
278
            extended_stats += ['num_handles']
279
280
        ret = {}
281
        try:
282
            # Get the extended stats
283
            selected_process = psutil.Process(proc['pid'])
284
            ret = selected_process.as_dict(attrs=extended_stats, ad_value=None)
285
286
            if LINUX:
287
                try:
288
                    ret['memory_swap'] = sum([v.swap for v in selected_process.memory_maps()])
289
                except (psutil.NoSuchProcess, KeyError):
290
                    # (KeyError catch for issue #1551)
291
                    pass
292
                except (psutil.AccessDenied, NotImplementedError):
293
                    # NotImplementedError: /proc/${PID}/smaps file doesn't exist
294
                    # on kernel < 2.6.14 or CONFIG_MMU kernel configuration option
295
                    # is not enabled (see psutil #533/glances #413).
296
                    ret['memory_swap'] = None
297
            try:
298
                ret['tcp'] = len(selected_process.connections(kind="tcp"))
299
                ret['udp'] = len(selected_process.connections(kind="udp"))
300
            except (psutil.AccessDenied, psutil.NoSuchProcess):
301
                # Manage issue1283 (psutil.AccessDenied)
302
                ret['tcp'] = None
303
                ret['udp'] = None
304
        except (psutil.NoSuchProcess, ValueError, AttributeError) as e:
305
            logger.error('Can not grab extended stats ({})'.format(e))
306
            self.extended_process = None
307
            ret['extended_stats'] = False
308
        else:
309
            logger.debug('Grab extended stats for process {}'.format(proc['pid']))
310
311
            # Compute CPU and MEM min/max/mean
312
            for stat_prefix in ['cpu', 'memory']:
313
                if stat_prefix + '_min' not in self.extended_process:
314
                    ret[stat_prefix + '_min'] = proc[stat_prefix + '_percent']
315
                else:
316
                    ret[stat_prefix + '_min'] = (
317
                        proc[stat_prefix + '_percent']
318
                        if proc[stat_prefix + '_min'] > proc[stat_prefix + '_percent']
319
                        else proc[stat_prefix + '_min']
320
                    )
321
                if stat_prefix + '_max' not in self.extended_process:
322
                    ret[stat_prefix + '_max'] = proc[stat_prefix + '_percent']
323
                else:
324
                    ret[stat_prefix + '_max'] = (
325
                        proc[stat_prefix + '_percent']
326
                        if proc[stat_prefix + '_max'] < proc[stat_prefix + '_percent']
327
                        else proc[stat_prefix + '_max']
328
                    )
329
                if stat_prefix + '_mean_sum' not in self.extended_process:
330
                    ret[stat_prefix + '_mean_sum'] = proc[stat_prefix + '_percent']
331
                else:
332
                    ret[stat_prefix + '_mean_sum'] = proc[stat_prefix + '_mean_sum'] + proc[stat_prefix + '_percent']
333
                if stat_prefix + '_mean_counter' not in self.extended_process:
334
                    ret[stat_prefix + '_mean_counter'] = 1
335
                else:
336
                    ret[stat_prefix + '_mean_counter'] = proc[stat_prefix + '_mean_counter'] + 1
337
                ret[stat_prefix + '_mean'] = ret[stat_prefix + '_mean_sum'] / ret[stat_prefix + '_mean_counter']
338
339
            ret['extended_stats'] = True
340
        return ret
341
342
    def is_selected_extended_process(self, position):
343
        """Return True if the process is the selected one for extended stats."""
344
        return (
345
            hasattr(self.args, 'programs')
346
            and not self.args.programs
347
            and hasattr(self.args, 'enable_process_extended')
348
            and self.args.enable_process_extended
349
            and not self.disable_extended_tag
350
            and hasattr(self.args, 'cursor_position')
351
            and position == self.args.cursor_position
352
            and not self.args.disable_cursor
353
        )
354
355
    def update(self):
356
        """Update the processes stats."""
357
        # Reset the stats
358
        self.processlist = []
359
        self.reset_processcount()
360
361
        # Do not process if disable tag is set
362
        if self.disable_tag:
363
            return
364
365
        # Time since last update (for disk_io rate computation)
366
        time_since_update = getTimeSinceLastUpdate('process_disk')
367
368
        # Grab standard stats
369
        #####################
370
        sorted_attrs = ['cpu_percent', 'cpu_times', 'memory_percent', 'name', 'status', 'num_threads']
371
        displayed_attr = ['memory_info', 'nice', 'pid']
372
        # 'name' can not be cached because it is used for filtering
373
        cached_attrs = ['cmdline', 'username']
374
375
        # Some stats are optional
376
        if not self.disable_io_counters:
377
            sorted_attrs.append('io_counters')
378
        if not self.disable_gids:
379
            displayed_attr.append('gids')
380
        # Some stats are not sort key
381
        # An optimisation can be done be only grabbed displayed_attr
382
        # for displayed processes (but only in standalone mode...)
383
        sorted_attrs.extend(displayed_attr)
384
        # Some stats are cached (not necessary to be refreshed every time)
385
        if self.cache_timer.finished():
386
            sorted_attrs += cached_attrs
387
            self.cache_timer.set(self.cache_timeout)
388
            self.cache_timer.reset()
389
            is_cached = False
390
        else:
391
            is_cached = True
392
393
        # Build the processes stats list (it is why we need psutil>=5.3.0)
394
        # This is one of the main bottleneck of Glances (see flame graph)
395
        # Filter processes
396
        self.processlist = list(
397
            filter(
398
                lambda p: not (BSD and p.info['name'] == 'idle')
399
                and not (WINDOWS and p.info['name'] == 'System Idle Process')
400
                and not (MACOS and p.info['name'] == 'kernel_task')
401
                and not (self.no_kernel_threads and LINUX and p.info['gids'].real == 0),
402
                psutil.process_iter(attrs=sorted_attrs, ad_value=None),
403
            )
404
        )
405
        # Only get the info key
406
        self.processlist = [p.info for p in self.processlist]
407
        # Sort the processes list by the current sort_key
408
        self.processlist = sort_stats(self.processlist, sorted_by=self.sort_key, reverse=True)
409
410
        # Update the processcount
411
        self.update_processcount(self.processlist)
412
413
        # Loop over processes and :
414
        # - add extended stats for selected process
415
        # - add metadata
416
        for position, proc in enumerate(self.processlist):
417
            # Extended stats
418
            ################
419
420
            # Get the selected process when the 'e' key is pressed
421
            if self.is_selected_extended_process(position):
422
                self.extended_process = proc
423
424
            # Grab extended stats only for the selected process (see issue #2225)
425
            if self.extended_process is not None and proc['pid'] == self.extended_process['pid']:
426
                proc.update(self.get_extended_stats(self.extended_process))
427
                self.extended_process = proc
428
429
            # Meta data
430
            ###########
431
432
            # PID is the key
433
            proc['key'] = 'pid'
434
435
            # Time since last update (for disk_io rate computation)
436
            proc['time_since_update'] = time_since_update
437
438
            # Process status (only keep the first char)
439
            proc['status'] = str(proc['status'])[:1].upper()
440
441
            # Process IO
442
            # procstat['io_counters'] is a list:
443
            # [read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag]
444
            # If io_tag = 0 > Access denied or first time (display "?")
445
            # If io_tag = 1 > No access denied (display the IO rate)
446
            if 'io_counters' in proc and proc['io_counters'] is not None:
447
                io_new = [proc['io_counters'].read_bytes, proc['io_counters'].write_bytes]
448
                # For IO rate computation
449
                # Append saved IO r/w bytes
450
                try:
451
                    proc['io_counters'] = io_new + self.io_old[proc['pid']]
452
                    io_tag = 1
453
                except KeyError:
454
                    proc['io_counters'] = io_new + [0, 0]
455
                    io_tag = 0
456
                # then save the IO r/w bytes
457
                self.io_old[proc['pid']] = io_new
458
            else:
459
                proc['io_counters'] = [0, 0] + [0, 0]
460
                io_tag = 0
461
            # Append the IO tag (for display)
462
            proc['io_counters'] += [io_tag]
463
464
            # Manage cached information
465
            if is_cached:
466
                # Grab cached values (in case of a new incoming process)
467
                if proc['pid'] not in self.processlist_cache:
468
                    try:
469
                        self.processlist_cache[proc['pid']] = psutil.Process(pid=proc['pid']).as_dict(
470
                            attrs=cached_attrs, ad_value=None
471
                        )
472
                    except psutil.NoSuchProcess:
473
                        pass
474
                # Add cached value to current stat
475
                try:
476
                    proc.update(self.processlist_cache[proc['pid']])
477
                except KeyError:
478
                    pass
479
            else:
480
                # Save values to cache
481
                self.processlist_cache[proc['pid']] = {cached: proc[cached] for cached in cached_attrs}
482
483
        # Apply user filter
484
        self.processlist = list(filter(lambda p: not self._filter.is_filtered(p), self.processlist))
485
486
        # Compute the maximum value for keys in self._max_values_list: CPU, MEM
487
        # Useful to highlight the processes with maximum values
488
        for k in self._max_values_list:
489
            values_list = [i[k] for i in self.processlist if i[k] is not None]
490
            if values_list:
491
                self.set_max_values(k, max(values_list))
492
493
    def get_count(self):
494
        """Get the number of processes."""
495
        return self.processcount
496
497
    def getlist(self, sorted_by=None, as_programs=False):
498
        """Get the processlist.
499
        By default, return the list of threads.
500
        If as_programs is True, return the list of programs."""
501
        if as_programs:
502
            return processes_to_programs(self.processlist)
503
        else:
504
            return self.processlist
505
506
    @property
507
    def sort_key(self):
508
        """Get the current sort key."""
509
        return self._sort_key
510
511
    def set_sort_key(self, key, auto=True):
512
        """Set the current sort key."""
513
        if key == 'auto':
514
            self.auto_sort = True
515
            self._sort_key = 'cpu_percent'
516
        else:
517
            self.auto_sort = auto
518
            self._sort_key = key
519
520
    def nice_decrease(self, pid):
521
        """Decrease nice level
522
        On UNIX this is a number which usually goes from -20 to 20.
523
        The higher the nice value, the lower the priority of the process."""
524
        p = psutil.Process(pid)
525
        try:
526
            p.nice(p.nice() - 1)
527
            logger.info('Set nice level of process {} to {} (higher the priority)'.format(pid, p.nice()))
528
        except psutil.AccessDenied:
529
            logger.warning(
530
                'Can not decrease (higher the priority) the nice level of process {} (access denied)'.format(pid)
531
            )
532
533
    def nice_increase(self, pid):
534
        """Increase nice level
535
        On UNIX this is a number which usually goes from -20 to 20.
536
        The higher the nice value, the lower the priority of the process."""
537
        p = psutil.Process(pid)
538
        try:
539
            p.nice(p.nice() + 1)
540
            logger.info('Set nice level of process {} to {} (lower the priority)'.format(pid, p.nice()))
541
        except psutil.AccessDenied:
542
            logger.warning(
543
                'Can not increase (lower the priority) the nice level of process {} (access denied)'.format(pid)
544
            )
545
546
    def kill(self, pid, timeout=3):
547
        """Kill process with pid"""
548
        assert pid != os.getpid(), "Glances can kill itself..."
549
        p = psutil.Process(pid)
550
        logger.debug('Send kill signal to process: {}'.format(p))
551
        p.kill()
552
        return p.wait(timeout)
553
554
555
def weighted(value):
556
    """Manage None value in dict value."""
557
    return -float('inf') if value is None else value
558
559
560
def _sort_io_counters(process, sorted_by='io_counters', sorted_by_secondary='memory_percent'):
561
    """Specific case for io_counters
562
563
    :return: Sum of io_r + io_w
564
    """
565
    return process[sorted_by][0] - process[sorted_by][2] + process[sorted_by][1] - process[sorted_by][3]
566
567
568
def _sort_cpu_times(process, sorted_by='cpu_times', sorted_by_secondary='memory_percent'):
569
    """Specific case for cpu_times
570
571
    Patch for "Sorting by process time works not as expected #1321"
572
    By default PsUtil only takes user time into account
573
    see (https://github.com/giampaolo/psutil/issues/1339)
574
    The following implementation takes user and system time into account
575
    """
576
    return process[sorted_by][0] + process[sorted_by][1]
577
578
579
def _sort_lambda(sorted_by='cpu_percent', sorted_by_secondary='memory_percent'):
580
    """Return a sort lambda function for the sorted_by key"""
581
    ret = None
582
    if sorted_by == 'io_counters':
583
        ret = _sort_io_counters
584
    elif sorted_by == 'cpu_times':
585
        ret = _sort_cpu_times
586
    return ret
587
588
589
def sort_stats(stats, sorted_by='cpu_percent', sorted_by_secondary='memory_percent', reverse=True):
590
    """Return the stats (dict) sorted by (sorted_by).
591
592
    Reverse the sort if reverse is True.
593
    """
594
    if sorted_by is None and sorted_by_secondary is None:
595
        # No need to sort...
596
        return stats
597
598
    # Check if a specific sort should be done
599
    sort_lambda = _sort_lambda(sorted_by=sorted_by, sorted_by_secondary=sorted_by_secondary)
600
601
    if sort_lambda is not None:
602
        # Specific sort
603
        try:
604
            stats.sort(key=sort_lambda, reverse=reverse)
605
        except Exception:
606
            # If an error is detected, fallback to cpu_percent
607
            stats.sort(
608
                key=lambda process: (weighted(process['cpu_percent']), weighted(process[sorted_by_secondary])),
609
                reverse=reverse,
610
            )
611
    else:
612
        # Standard sort
613
        try:
614
            stats.sort(
615
                key=lambda process: (weighted(process[sorted_by]), weighted(process[sorted_by_secondary])),
616
                reverse=reverse,
617
            )
618
        except (KeyError, TypeError):
619
            # Fallback to name
620
            stats.sort(key=lambda process: process['name'] if process['name'] is not None else '~', reverse=False)
621
622
    return stats
623
624
625
glances_processes = GlancesProcesses()
626