Correct merge conflict

This commit is contained in:
nicolargo 2022-03-20 09:09:00 +01:00
commit 40864d35e1
20 changed files with 781 additions and 575 deletions

View File

@ -244,7 +244,7 @@ To install the binary package:
.. code-block:: console
# pkg install py37-glances
# pkg install py38-glances
To install Glances from ports:

View File

@ -59,6 +59,9 @@ You can also set the sort key in the UI:
* - i
- --sort-processes io_counters
- Sort by DISK I/O
* - j
- --programs
- Accumulate processes by program
* - m
- --sort-processes memory_percent
- Sort by MEM
@ -108,7 +111,7 @@ Columns display
The non-swapped physical memory a process is
using (what's currently in the physical memory).
``PID`` Process ID
``PID`` Process ID (column is replaced by NPROCS in accumulated mode)
``USER`` User ID
``THR`` Threads number of the process
``TIME+`` Cumulative CPU time used by the process
@ -133,8 +136,6 @@ Columns display
pressing on the ``'/'`` key
========================= ==============================================
Source: Thanks to the Peteris Ņikiforovs's blog.
Process filtering
-----------------
@ -180,3 +181,11 @@ In curses/standalone mode, you can select a process using ``UP`` and ``DOWN`` an
configuration file under the ``[processlist]`` section. It is also
possible to define limit for Nice values (comma separated list).
For example: nice_warning=-20,-19,-18
Accumulated per program — key 'j'
---------------------------------
When activated ('j' hotkey or --programs option in the command line), processes are merged
to display which programs are active. The columns show the accumulated cpu consumption, the
accumulated virtual and resident memory consumption, the accumulated transferred data I/O.
The PID columns is replaced by a NPROCS column which is the number of processes.

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "GLANCES" "1" "Jan 15, 2022" "3.2.5_beta01" "Glances"
.TH "GLANCES" "1" "Mar 13, 2022" "3.2.5_beta01" "Glances"
.SH NAME
glances \- An eye on your system
.

View File

@ -29,6 +29,7 @@ import unicodedata
import types
import subprocess
import os
from datetime import datetime
from glances.logger import logger
@ -311,3 +312,47 @@ def enable(class_name, var):
"""Set disable_<var> to False in the class class_name."""
setattr(class_name, 'enable_' + var, True)
setattr(class_name, 'disable_' + var, False)
def pretty_date(time=False):
"""
Get a datetime object or a int() Epoch timestamp and return a
pretty string like 'an hour ago', 'Yesterday', '3 months ago',
'just now', etc
Source: https://stackoverflow.com/questions/1551382/user-friendly-time-format-in-python
"""
now = datetime.now()
if type(time) is int:
diff = now - datetime.fromtimestamp(time)
elif isinstance(time, datetime):
diff = now - time
elif not time:
diff = 0
second_diff = diff.seconds
day_diff = diff.days
if day_diff < 0:
return ''
if day_diff == 0:
if second_diff < 10:
return "just now"
if second_diff < 60:
return str(second_diff) + " seconds"
if second_diff < 120:
return "a minute"
if second_diff < 3600:
return str(second_diff // 60) + " minutes"
if second_diff < 7200:
return "an hour"
if second_diff < 86400:
return str(second_diff // 3600) + " hours"
if day_diff == 1:
return "Yesterday"
if day_diff < 7:
return str(day_diff) + " days"
if day_diff < 31:
return str(day_diff // 7) + " weeks"
if day_diff < 365:
return str(day_diff // 30) + " months"
return str(day_diff // 365) + " years"

View File

@ -252,6 +252,15 @@ Examples of use:
choices=sort_processes_key_list,
help='Sort processes by: {}'.format(', '.join(sort_processes_key_list)),
)
# Display processes list by program name and not by thread
parser.add_argument(
'--programs',
'--program',
action='store_true',
default=False,
dest='programs',
help='Accumulate processes by program',
)
# Export modules feature
parser.add_argument('--export', dest='export', help='enable export module (comma separed list)')
parser.add_argument(
@ -340,7 +349,7 @@ Examples of use:
default=self.DEFAULT_REFRESH_TIME,
type=float,
dest='time',
help='set minumum refresh rate in seconds [default: {} sec]'.format(self.DEFAULT_REFRESH_TIME),
help='set minimum refresh rate in seconds [default: {} sec]'.format(self.DEFAULT_REFRESH_TIME),
)
parser.add_argument(
'-w',

View File

@ -76,6 +76,7 @@ class _GlancesCurses(object):
'h': {'switch': 'help_tag'},
'i': {'sort_key': 'io_counters'},
'I': {'switch': 'disable_ip'},
'j': {'switch': 'programs'},
# 'k' > Kill selected process
'K': {'switch': 'disable_connections'},
'l': {'switch': 'disable_alert'},
@ -691,27 +692,10 @@ class _GlancesCurses(object):
# Display kill process confirmation popup
# Only in standalone mode (cs_status is None)
if self.kill_process and cs_status is None:
selected_process_raw = stats.get_plugin('processlist').get_raw()[self.args.cursor_position]
confirm = self.display_popup(
'Kill process: {} (pid: {}) ?\n\nConfirm ([y]es/[n]o): '.format(
selected_process_raw['name'], selected_process_raw['pid']
),
popup_type='yesno',
)
if confirm.lower().startswith('y'):
try:
ret_kill = glances_processes.kill(selected_process_raw['pid'])
except Exception as e:
logger.error('Can not kill process {} ({})'.format(selected_process_raw['name'], e))
else:
logger.info(
'Kill signal has been sent to process {} (return code: {})'.format(
selected_process_raw['name'], ret_kill
)
)
logger.info(stats.get_plugin('processlist').get_raw()[self.args.cursor_position])
self.kill(stats.get_plugin('processlist').get_raw()[self.args.cursor_position])
elif self.kill_process and cs_status is not None:
self.display_popup('Kill process only available in standalone mode')
self.display_popup('Kill process only available for local processes')
self.kill_process = False
# Display graph generation popup
@ -720,6 +704,40 @@ class _GlancesCurses(object):
return True
def kill(self, process):
"""Kill a process, or a list of process if the process has a childrens field.
:param process
:return: None
"""
logger.debug("Selected process to kill: {}".format(process))
if 'childrens' in process:
pid_to_kill = process['childrens']
else:
pid_to_kill = [process['pid']]
confirm = self.display_popup(
'Kill process: {} (pid: {}) ?\n\nConfirm ([y]es/[n]o): '.format(
process['name'],
', '.join(map(str,pid_to_kill)),
),
popup_type='yesno',
)
if confirm.lower().startswith('y'):
for pid in pid_to_kill:
try:
ret_kill = glances_processes.kill(pid)
except Exception as e:
logger.error('Can not kill process {} ({})'.format(pid, e))
else:
logger.info(
'Kill signal has been sent to process {} (return code: {})'.format(
pid, ret_kill
)
)
def __display_header(self, stat_display):
"""Display the firsts lines (header) in the Curses interface.

View File

@ -46,6 +46,14 @@ export default function GlancesController($scope, GlancesStats, hotkeys, ARGUMEN
}
});
// j => Accumulate processes by program
hotkeys.add({
combo: 'j',
callback: function () {
ARGUMENTS.programs = !ARGUMENTS.programs;
}
});
// k => Show/hide connections stats
hotkeys.add({
combo: 'k',

View File

@ -17,6 +17,7 @@ export default function GlancesPluginDockerController($scope, GlancesStats) {
'id': containerData.Id,
'name': containerData.name,
'status': containerData.Status,
'uptime': containerData.Uptime,
'cpu': containerData.cpu.total,
'memory': containerData.memory.usage != undefined ? containerData.memory.usage : '?',
'rss': containerData.memory.rss != undefined ? containerData.memory.rss : '?',

View File

@ -5,6 +5,7 @@
<div class="table-row">
<div class="table-cell text-left">Name</div>
<div class="table-cell">Status</div>
<div class="table-cell">Uptime</div>
<div class="table-cell">CPU%</div>
<div class="table-cell">MEM</div>
<div class="table-cell">RSS</div>
@ -18,6 +19,8 @@
<div class="table-cell text-left">{{ container.name }}</div>
<div class="table-cell" ng-class="container.status == 'Paused' ? 'careful' : 'ok'">{{ container.status }}
</div>
<div class="table-cell" ng-class="container.status == 'Paused' ? 'careful' : 'ok'">{{ container.uptime }}
</div>
<div class="table-cell">{{ container.cpu | number:1 }}</div>
<div class="table-cell">{{ container.memory | bytes }}</div>
<div class="table-cell">{{ container.rss | bytes }}</div>

Binary file not shown.

Binary file not shown.

View File

@ -189,4 +189,4 @@ class ThreadOpenStack(threading.Thread):
def stopped(self):
"""Return True is the thread is stopped."""
return self._stopper.isSet()
return self._stopper.is_set()

View File

@ -23,7 +23,9 @@ import os
import threading
import time
from copy import deepcopy
from dateutil import parser
from glances.compat import iterkeys, itervalues, nativestr, pretty_date
from glances.logger import logger
from glances.globals import iterkeys, itervalues, nativestr
from glances.timer import getTimeSinceLastUpdate
@ -291,6 +293,8 @@ class PluginModel(GlancesPluginModel):
)
container_stats['network_rx'] = container_stats['network'].get('rx', None)
container_stats['network_tx'] = container_stats['network'].get('tx', None)
# Uptime
container_stats['Uptime'] = pretty_date(parser.parse(container.attrs['State']['StartedAt']).replace(tzinfo=None))
else:
container_stats['cpu'] = {}
container_stats['cpu_percent'] = None
@ -302,6 +306,7 @@ class PluginModel(GlancesPluginModel):
container_stats['network'] = {}
container_stats['network_rx'] = None
container_stats['network_tx'] = None
container_stats['Uptime'] = None
# Add current container stats to the stats list
stats['containers'].append(container_stats)
@ -556,6 +561,8 @@ class PluginModel(GlancesPluginModel):
ret.append(self.curse_add_line(msg))
msg = '{:>10}'.format('Status')
ret.append(self.curse_add_line(msg))
msg = '{:>10}'.format('Uptime')
ret.append(self.curse_add_line(msg))
msg = '{:>6}'.format('CPU%')
ret.append(self.curse_add_line(msg))
msg = '{:>7}'.format('MEM')
@ -581,6 +588,12 @@ class PluginModel(GlancesPluginModel):
status = self.container_alert(container['Status'])
msg = '{:>10}'.format(container['Status'][0:10])
ret.append(self.curse_add_line(msg, status))
# Uptime
if container['Uptime']:
msg = '{:>10}'.format(container['Uptime'])
else:
msg = '{:>10}'.format('_')
ret.append(self.curse_add_line(msg, status))
# CPU
try:
msg = '{:>6.1f}'.format(container['cpu']['total'])
@ -712,7 +725,7 @@ class ThreadDockerGrabber(threading.Thread):
def stopped(self):
"""Return True is the thread is stopped."""
return self._stopper.isSet()
return self._stopper.is_set()
def sort_stats(stats):

View File

@ -110,6 +110,7 @@ class PluginModel(GlancesPluginModel):
self.view_data['show_hide_raid_plugin'] = msg_col.format('R', 'Show/hide RAID plugin')
self.view_data['show_hide_wifi_module'] = msg_col2.format('W', 'Show/hide Wifi module')
self.view_data['enable_disable_all_but_quick_look_and_load_module'] = msg_col2.format('4', 'Enable/disable all but quick look and load module')
self.view_data['programs'] = msg_col2.format('j', 'Accumulate processes by program')
def get_view_data(self, args=None):
@ -203,7 +204,8 @@ class PluginModel(GlancesPluginModel):
ret.append(self.curse_add_line(self.view_data['enable_disable_all_but_quick_look_and_load_module']))
ret.append(self.curse_new_line())
ret.append(self.curse_add_line(self.view_data['quit']))
ret.append(self.curse_add_line(self.view_data['programs']))
ret.append(self.curse_new_line())
ret.append(self.curse_new_line())

View File

@ -255,7 +255,7 @@ class ThreadScanner(threading.Thread):
def stopped(self):
"""Return True is the thread is stopped."""
return self._stopper.isSet()
return self._stopper.is_set()
def _web_scan(self, web):
"""Scan the Web/URL (dict) and update the status key."""

View File

@ -141,16 +141,17 @@ class PluginModel(GlancesPluginModel):
ret.append(self.curse_add_line(msg))
# Display sort information
msg = 'Programs' if self.args.programs else 'Threads'
try:
sort_human = self.sort_for_human[glances_processes.sort_key]
except KeyError:
sort_human = glances_processes.sort_key
if glances_processes.auto_sort:
msg = 'sorted automatically'
msg += ' sorted automatically'
ret.append(self.curse_add_line(msg))
msg = ' by {}'.format(sort_human)
else:
msg = 'sorted by {}'.format(sort_human)
msg += ' sorted by {}'.format(sort_human)
ret.append(self.curse_add_line(msg))
# Return the message with decoration

View File

@ -2,7 +2,7 @@
#
# This file is part of Glances.
#
# Copyright (C) 2021 Nicolargo <nicolas@nicolargo.com>
# Copyright (C) 2022 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
@ -28,6 +28,7 @@ from glances.processes import glances_processes, sort_stats
from glances.outputs.glances_unicode import unicode_message
from glances.plugins.core.model import PluginModel as CorePluginModel
from glances.plugins.plugin.model import GlancesPluginModel
from glances.programs import processes_to_programs
def seconds_to_hms(input_seconds):
@ -155,6 +156,9 @@ class PluginModel(GlancesPluginModel):
# Note: Update is done in the processcount plugin
# Just return the processes list
stats = glances_processes.getlist()
if self.args.programs:
stats = processes_to_programs(stats)
elif self.input_method == 'snmp':
# No SNMP grab for processes
@ -375,7 +379,15 @@ class PluginModel(GlancesPluginModel):
ret.append(self._get_process_curses_rss(p, selected, args))
# PID
msg = self.layout_stat['pid'].format(p['pid'], width=self.__max_pid_size())
if not self.args.programs:
# Display processes, so the PID should be displayed
msg = self.layout_stat['pid'].format(p['pid'],
width=self.__max_pid_size())
else:
# Display programs, so the PID should not be displayed
# Instead displays the number of childrens
msg = self.layout_stat['pid'].format(len(p['childrens']) if 'childrens' in p else '_',
width=self.__max_pid_size())
ret.append(self.curse_add_line(msg))
# USER
@ -546,7 +558,10 @@ class PluginModel(GlancesPluginModel):
ret.append(self.curse_add_line(msg, optional=True))
msg = self.layout_header['res'].format('RES')
ret.append(self.curse_add_line(msg, optional=True))
msg = self.layout_header['pid'].format('PID', width=self.__max_pid_size())
if not self.args.programs:
msg = self.layout_header['pid'].format('PID', width=self.__max_pid_size())
else:
msg = self.layout_header['pid'].format('NPROCS', width=self.__max_pid_size())
ret.append(self.curse_add_line(msg))
msg = self.layout_header['user'].format('USER')
ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'username' else 'DEFAULT'))
@ -572,7 +587,10 @@ class PluginModel(GlancesPluginModel):
msg, sort_style if process_sort_key == 'io_counters' else 'DEFAULT', optional=True, additional=True
)
)
msg = self.layout_header['command'].format('Command', "('k' to kill)" if args.is_standalone else "")
if not self.args.programs:
msg = self.layout_header['command'].format('Command', "('k' to kill)" if args.is_standalone else "")
else:
msg = self.layout_header['command'].format('Programs', "('k' to kill)" if args.is_standalone else "")
ret.append(self.curse_add_line(msg, sort_style if process_sort_key == 'name' else 'DEFAULT'))
def __msg_curse_sum(self, ret, sep_char='_', mmm=None, args=None):

67
glances/programs.py Normal file
View File

@ -0,0 +1,67 @@
# -*- coding: utf-8 -*-
#
# This file is part of Glances.
#
# Copyright (C) 2022 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/>.
# from glances.logger import logger
# This constant defines the list of available processes sort key
sort_programs_key_list = ['cpu_percent', 'memory_percent', 'cpu_times', 'io_counters', 'name']
def processes_to_programs(processes):
"""Convert a list of processes to a list of programs."""
# Start to build a dict of programs (key is program name)
programs_dict = {}
key = 'name'
for p in processes:
if p[key] not in programs_dict:
# Create a new entry in the dict (new program)
programs_dict[p[key]] = {
'time_since_update': p['time_since_update'],
'num_threads': p['num_threads'],
'cpu_percent': p['cpu_percent'],
'memory_percent': p['memory_percent'],
'cpu_times': p['cpu_times'],
'memory_info': p['memory_info'],
'io_counters': p['io_counters'],
'childrens': [p['pid']],
# Others keys are not used
# but should be set to be compliant with the existing process_list
'name': p['name'],
'cmdline': [p['name']],
'pid': '_',
'username': p['username'],
'nice': p['nice'],
'status': p['status'],
}
else:
# Update a existing entry in the dict (existing program)
programs_dict[p[key]]['num_threads'] += p['num_threads']
programs_dict[p[key]]['cpu_percent'] += p['cpu_percent']
programs_dict[p[key]]['memory_percent'] += p['memory_percent']
programs_dict[p[key]]['cpu_times'] += p['cpu_times']
programs_dict[p[key]]['memory_info'] += p['memory_info']
programs_dict[p[key]]['io_counters'] += p['io_counters']
programs_dict[p[key]]['childrens'].append(p['pid'])
# If all the subprocess has the same value, display it
programs_dict[p[key]]['username'] = p['username'] if p['username'] == programs_dict[p[key]]['username'] else '_'
programs_dict[p[key]]['nice'] = p['nice'] if p['nice'] == programs_dict[p[key]]['nice'] else '_'
programs_dict[p[key]]['status'] = p['status'] if p['status'] == programs_dict[p[key]]['status'] else '_'
# Convert the dict to a list of programs
return [programs_dict[p] for p in programs_dict]

View File

@ -21,7 +21,6 @@
"""Glances unitary tests suite."""
import time
from tracemalloc import Snapshot
import unittest
import sys
@ -41,8 +40,11 @@ from glances.thresholds import GlancesThresholdWarning
from glances.thresholds import GlancesThresholdCritical
from glances.thresholds import GlancesThresholds
from glances.plugins.plugin.model import GlancesPluginModel
from glances.programs import processes_to_programs
from glances.compat import subsample, range
from glances.secure import secure_popen
from glances.compat import PY3
from tracemalloc import Snapshot
# Global variables
# =================
@ -286,6 +288,16 @@ class TestGlances(unittest.TestCase):
print('INFO: SMART stats: %s' % stats_grab)
def test_017_programs(self):
"""Check Programs function (it's not a plugin)."""
# stats_to_check = [ ]
print('INFO: [TEST_010] Check PROGRAM stats')
stats_grab = processes_to_programs(stats.get_plugin('processlist').get_raw())
self.assertTrue(type(stats_grab) is list, msg='Programs stats is not a list')
print('INFO: PROGRAM list stats: %s items in the list' % len(stats_grab))
# Check if number of processes in the list equal counter
# self.assertEqual(total, len(stats_grab))
def test_094_thresholds(self):
"""Test thresholds classes"""
print('INFO: [TEST_094] Thresholds')
@ -394,9 +406,6 @@ class TestGlances(unittest.TestCase):
def test_200_memory_leak(self):
"""Memory leak check"""
# Only available in PY3
if not PY3:
return
import tracemalloc
print('INFO: [TEST_200] Memory leak check')
tracemalloc.start()