diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 46d4fe7b..95522c53 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -6,6 +6,9 @@ labels: '' assignees: '' --- +**Check the bug** +Before filling this bug report, please search if a similar issue already exists. +In this case, just add a comment on this existing issue. **Describe the bug** A clear and concise description of what the bug is. @@ -26,11 +29,7 @@ If applicable, add screenshots to help explain your problem. - Operating System (lsb_release -a or OS name/version): `To be completed with result of: lsb_release -a` - Glances & psutil versions: `To be completed with result of: glances -V` - How do you install Glances (Pypi package, script, package manager, source): `To be completed` - - Glances test (only available with Glances 3.1.7 or higher): - - ``` - To be completed with result of: glances --issue - ``` + - Glances test: ` To be completed with result of: glances --issue` **Additional context** Add any other context about the problem here. diff --git a/.github/workflows/inactive_issues.yml b/.github/workflows/inactive_issues.yml new file mode 100644 index 00000000..21762cbb --- /dev/null +++ b/.github/workflows/inactive_issues.yml @@ -0,0 +1,22 @@ +name: Label inactive issues +on: + schedule: + - cron: "30 1 * * *" + +jobs: + close-issues: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v5 + with: + days-before-issue-stale: 90 + days-before-issue-close: -1 + stale-issue-label: "inactive" + stale-issue-message: "This issue is stale because it has been open for 3 months with no activity." + close-issue-message: "This issue was closed because it has been inactive for 30 days since being marked as stale." + days-before-pr-stale: -1 + days-before-pr-close: -1 + repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/needs_contributor.yml b/.github/workflows/needs_contributor.yml new file mode 100644 index 00000000..b9a5bb8b --- /dev/null +++ b/.github/workflows/needs_contributor.yml @@ -0,0 +1,22 @@ +name: Add a message when needs contributor tag is used +on: + issues: + types: + - labeled +jobs: + add-comment: + if: github.event.label.name == 'needs contributor' + runs-on: ubuntu-latest + permissions: + issues: write + steps: + - name: Add comment + run: gh issue comment "$NUMBER" --body "$BODY" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + NUMBER: ${{ github.event.issue.number }} + BODY: > + This issue is available for anyone to work on. + **Make sure to reference this issue in your pull request.** + :sparkles: Thank you for your contribution ! :sparkles: \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..cf9b7a17 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,34 @@ +# Read the Docs configuration file for Glances projects + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + # You can also specify other tool versions: + # nodejs: "20" + # rust: "1.70" + # golang: "1.20" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs + # builder: "dirhtml" + # Fail on all warnings to avoid broken references + # fail_on_warning: true + +# Optionally build your docs in additional formats such as PDF and ePub +# formats: +# - pdf +# - epub + +# Optional but recommended, declare the Python requirements required +# to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: doc-requirements.txt \ No newline at end of file diff --git a/NEWS.rst b/NEWS.rst index 19ca2061..f55db2a9 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -3,62 +3,39 @@ ============================================================================== =============== -Version 4.0.8 +Version 4.1.0 =============== -* Make CORS option configurable security webui #2812 -* When Glances is installed via venv, default configuration file is not used documentation packaging #2803 -* GET /1272f6e9e8f9d6bfd6de.png results in 404 bug webui #2781 by Emporea was closed May 25, 2024 -* Screen frequently flickers when outputting to local display bug needs test #2490 -* Retire ujson for being in maintenance mode dependencies enhancement #2791 +Enhancements: -Minor breaking change in AMP: please use && instead of ; as command line separator. +* Call process_iter.clear_cache() (PsUtil 6+) when Glances user force a refresh (F5 or CTRL-R) #2753 +* PsUtil 6+ no longer check PID reused #2755 +* Add support for automatically hiding network interfaces that are down or that don't have any IP addresses #2799 -=============== -Version 4.0.7 -=============== +Bug corrected: -* cpu_hz_current not available on NetBSD #2792 -* SensorType change in REST API breaks compatibility in 4.0.4 #2788 +* API: Network module is disabled but appears in endpoint "all" #2815 +* API is not compatible with requests containing spcial/encoding char #2820 +* 'j' hot key crashs Glances #2831 +* Raspberry PI - CPU info is not correct #2616 +* Graph export is broken if there is no graph section in Glances configuration file #2839 +* Glances API status check returns Error 405 - Method Not Allowed #2841 +* Rootless podman containers cause glances to fail with KeyError #2827 +* --export-process-filter Filter using complete command #2824 +* Exception when Glances is ran with limited plugin list #2822 +* Disable separator option do not work #2823 -=============== -Version 4.0.6 -=============== +Continious integration and documentation: -* No GPU info on Web View #2796 +* test test_107_fs_plugin_method fails on aarch64-linux #2819 -=============== -Version 4.0.5 -=============== +Thanks to all contibutors and bug reporters ! -* SensorType change in REST API breaks compatibility in 4.0.4 #2788 -* Please make pydantic optional dependency, not required one #2777 -* Update the Grafana dashboard #2780 -* 4.0.4 - On Glances startup "ERROR -- Can not init battery class #2776 -* In codeSpace (with Python 3.8), an error occurs in ./unittest-restful.py #2773 +Special thanks to: -Use Ruff as default Linter. - -=============== -Version 4.0.4 -=============== - -Hostfix release for support sensors plugin on python 3.8 - -=============== -Version 4.0.3 -=============== - -Additional fixes for Sensor plugin - -=============== -Version 4.0.2 -=============== - -* hotfix: plugin(sensors) - race conditions btw fan_speed & temperature… #2766 -* fix: include requirements.txt and SECURITY.md for pypi dist #2761 - -Thanks to RazCrimson for the sensors patch ! +* Bharath Vignesh J K +* RazCrimson +* Vadim Smal =============== Version 4.0.8 diff --git a/README.rst b/README.rst index 7bee0ed3..c5f064ba 100644 --- a/README.rst +++ b/README.rst @@ -221,8 +221,6 @@ Run last version of Glances container in *console mode*: By default, the /etc/glances/glances.conf file is used (based on docker-compose/glances.conf). -By default, the /etc/glances/glances.conf file is used (based on docker-compose/glances.conf). - Additionally, if you want to use your own glances.conf file, you can create your own Dockerfile: diff --git a/SECURITY.md b/SECURITY.md index 5b2c9628..7b112925 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,13 +2,10 @@ ## Supported Versions -Use this section to tell people about which versions of your project are -currently being supported with security updates. - | Version | Support security updates | | ------- | ------------------------ | -| 3.x | :white_check_mark: | -| < 3.0 | :x: | +| 4.x | :white_check_mark: | +| < 4.0 | :x: | ## Reporting a Vulnerability @@ -31,4 +28,3 @@ If there are any vulnerabilities in {{cookiecutter.project_name}}, don't hesitat 4. Please do not disclose the vulnerability publicly until a fix is released! Once we have either a) published a fix, or b) declined to address the vulnerability for whatever reason, you are free to publicly disclose it. - diff --git a/conf/glances.conf b/conf/glances.conf index 4de3d3a9..7bba30aa 100644 --- a/conf/glances.conf +++ b/conf/glances.conf @@ -26,7 +26,7 @@ history_size=1200 # Options for all UIs #-------------------- # Separator in the Curses and WebUI interface (between top and others plugins) -separator=True +#separator=True # Set the the Curses and WebUI interface left menu plugin list (comma-separated) #left_menu=network,wifi,connections,ports,diskio,fs,irq,folders,raid,smart,sensors,now # Limit the number of processes to display (in the WebUI) @@ -217,9 +217,9 @@ hide=docker.*,lo # Define the list of wireless network interfaces to be show (comma-separated) #show=docker.* # Automatically hide interface not up (default is False) -#hide_no_up=True +hide_no_up=True # Automatically hide interface with no IP address (default is False) -#hide_no_ip=True +hide_no_ip=True # It is possible to overwrite the bitrate thresholds per interface # WLAN 0 Default limits (in bits per second aka bps) for interface bitrate #wlan0_rx_careful=4000000 diff --git a/doc-requirements.txt b/doc-requirements.txt index 74d6796a..28ee5654 100644 --- a/doc-requirements.txt +++ b/doc-requirements.txt @@ -1,3 +1,5 @@ +psutil +defusedxml orjson reuse setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/docker-compose/glances.conf b/docker-compose/glances.conf index 792a155d..11bcb951 100755 --- a/docker-compose/glances.conf +++ b/docker-compose/glances.conf @@ -26,7 +26,7 @@ history_size=1200 # Options for all UIs #-------------------- # Separator in the Curses and WebUI interface (between top and others plugins) -separator=True +#separator=True # Set the the Curses and WebUI interface left menu plugin list (comma-separated) #left_menu=network,wifi,connections,ports,diskio,fs,irq,folders,raid,smart,sensors,now # Limit the number of processes to display (in the WebUI) @@ -60,7 +60,7 @@ max_processes_display=25 #cors_headers=* ############################################################################## -# plugins +# Plugins ############################################################################## [quicklook] diff --git a/docs/aoa/network.rst b/docs/aoa/network.rst index d6b02d91..8359ad0e 100644 --- a/docs/aoa/network.rst +++ b/docs/aoa/network.rst @@ -48,7 +48,7 @@ virtual docker interface (docker0, docker1, ...): # Automatically hide interface with no IP address (default is False) hide_no_ip=True # WLAN 0 alias - wlan0_alias=Wireless IF + alias=wlan0:Wireless IF # It is possible to overwrite the bitrate thresholds per interface # WLAN 0 Default limits (in bits per second aka bps) for interface bitrate wlan0_rx_careful=4000000 @@ -64,4 +64,4 @@ Filtering is based on regular expression. Please be sure that your regular expression works as expected. You can use an online tool like `regex101`_ in order to test your regular expression. -.. _regex101: https://regex101.com/ \ No newline at end of file +.. _regex101: https://regex101.com/ diff --git a/docs/aoa/sensors.rst b/docs/aoa/sensors.rst index 87d64933..5ee5923f 100644 --- a/docs/aoa/sensors.rst +++ b/docs/aoa/sensors.rst @@ -27,8 +27,7 @@ There is no alert on this information. .. note 3:: If a sensors has temperature and fan speed with the same name unit, it is possible to alias it using: - unitname_temperature_core_alias=Alias for temp - unitname_fan_speed_alias=Alias for fan speed + alias=unitname_temperature_core_alias:Alias for temp,unitname_fan_speed_alias:Alias for fan speed .. note 4:: If a sensors has multiple identical features names (see #2280), then diff --git a/docs/api.rst b/docs/api.rst index e6b058fa..58999ba0 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -141,7 +141,7 @@ Get plugin stats:: "refresh": 3.0, "regex": True, "result": None, - "timer": 0.24439311027526855}, + "timer": 0.48795127868652344}, {"count": 0, "countmax": 20.0, "countmin": None, @@ -150,7 +150,7 @@ Get plugin stats:: "refresh": 3.0, "regex": True, "result": None, - "timer": 0.2443389892578125}] + "timer": 0.48785948753356934}] Fields descriptions: @@ -178,7 +178,7 @@ Get a specific item when field matches the given value:: "refresh": 3.0, "regex": True, "result": None, - "timer": 0.24439311027526855}]} + "timer": 0.48795127868652344}]} GET cloud --------- @@ -219,21 +219,7 @@ GET containers Get plugin stats:: # curl http://localhost:61208/api/4/containers - [{"command": "/bin/sh -c /venv/bin/python3 -m glances $GLANCES_OPT", - "cpu": {"total": 0.0}, - "cpu_percent": 0.0, - "created": "2024-05-25T13:52:22.535373707Z", - "engine": "docker", - "id": "bb99d31288db8904ed4cd43db8255a926830936189bc180d77c3459cbaa7f490", - "image": ["nicolargo/glances:latest"], - "io": {}, - "key": "name", - "memory": {}, - "memory_usage": None, - "name": "wizardly_nightingale", - "network": {}, - "status": "running", - "uptime": "14 mins"}] + [] Fields descriptions: @@ -254,31 +240,6 @@ Fields descriptions: * **pod_name**: Pod name (only with Podman) (unit is *None*) * **pod_id**: Pod ID (only with Podman) (unit is *None*) -Get a specific field:: - - # curl http://localhost:61208/api/4/containers/name - {"name": ["wizardly_nightingale"]} - -Get a specific item when field matches the given value:: - - # curl http://localhost:61208/api/4/containers/name/wizardly_nightingale - {"wizardly_nightingale": [{"command": "/bin/sh -c /venv/bin/python3 -m glances " - "$GLANCES_OPT", - "cpu": {"total": 0.0}, - "cpu_percent": 0.0, - "created": "2024-05-25T13:52:22.535373707Z", - "engine": "docker", - "id": "bb99d31288db8904ed4cd43db8255a926830936189bc180d77c3459cbaa7f490", - "image": ["nicolargo/glances:latest"], - "io": {}, - "key": "name", - "memory": {}, - "memory_usage": None, - "name": "wizardly_nightingale", - "network": {}, - "status": "running", - "uptime": "14 mins"}]} - GET core -------- @@ -304,19 +265,19 @@ Get plugin stats:: # curl http://localhost:61208/api/4/cpu {"cpucore": 16, - "ctx_switches": 7772781, + "ctx_switches": 426798284, "guest": 0.0, - "idle": 3.0, - "interrupts": 6643340, - "iowait": 0.0, + "idle": 85.4, + "interrupts": 358987449, + "iowait": 0.1, "irq": 0.0, "nice": 0.0, - "soft_interrupts": 1761276, + "soft_interrupts": 133317922, "steal": 0.0, "syscalls": 0, - "system": 0.0, - "total": 16.7, - "user": 1.0} + "system": 3.2, + "total": 6.7, + "user": 11.4} Fields descriptions: @@ -349,7 +310,7 @@ Fields descriptions: Get a specific field:: # curl http://localhost:61208/api/4/cpu/total - {"total": 16.7} + {"total": 6.7} GET diskio ---------- @@ -359,14 +320,14 @@ Get plugin stats:: # curl http://localhost:61208/api/4/diskio [{"disk_name": "nvme0n1", "key": "disk_name", - "read_bytes": 3983976960, - "read_count": 115648, - "write_bytes": 3073111040, - "write_count": 91409}, + "read_bytes": 7464889856, + "read_count": 275684, + "write_bytes": 24858043392, + "write_count": 1204326}, {"disk_name": "nvme0n1p1", "key": "disk_name", - "read_bytes": 7476224, - "read_count": 576, + "read_bytes": 7558144, + "read_count": 605, "write_bytes": 1024, "write_count": 2}] @@ -402,10 +363,10 @@ Get a specific item when field matches the given value:: # curl http://localhost:61208/api/4/diskio/disk_name/nvme0n1 {"nvme0n1": [{"disk_name": "nvme0n1", "key": "disk_name", - "read_bytes": 3983976960, - "read_count": 115648, - "write_bytes": 3073111040, - "write_count": 91409}]} + "read_bytes": 7464889856, + "read_count": 275684, + "write_bytes": 24858043392, + "write_count": 1204326}]} GET folders ----------- @@ -432,13 +393,13 @@ Get plugin stats:: # curl http://localhost:61208/api/4/fs [{"device_name": "/dev/mapper/ubuntu--vg-ubuntu--lv", - "free": 902284546048, + "free": 896615567360, "fs_type": "ext4", "key": "mnt_point", "mnt_point": "/", - "percent": 5.3, + "percent": 5.9, "size": 1003736440832, - "used": 50389389312}] + "used": 56058368000}] Fields descriptions: @@ -459,13 +420,13 @@ Get a specific item when field matches the given value:: # curl http://localhost:61208/api/4/fs/mnt_point// {"/": [{"device_name": "/dev/mapper/ubuntu--vg-ubuntu--lv", - "free": 902284546048, + "free": 896615567360, "fs_type": "ext4", "key": "mnt_point", "mnt_point": "/", - "percent": 5.3, + "percent": 5.9, "size": 1003736440832, - "used": 50389389312}]} + "used": 56058368000}]} GET gpu ------- @@ -539,9 +500,9 @@ Get plugin stats:: # curl http://localhost:61208/api/4/load {"cpucore": 16, - "min1": 0.560546875, - "min15": 0.54833984375, - "min5": 0.70166015625} + "min1": 0.69091796875, + "min15": 0.93115234375, + "min5": 0.9248046875} Fields descriptions: @@ -553,7 +514,7 @@ Fields descriptions: Get a specific field:: # curl http://localhost:61208/api/4/load/min1 - {"min1": 0.560546875} + {"min1": 0.69091796875} GET mem ------- @@ -561,16 +522,16 @@ GET mem Get plugin stats:: # curl http://localhost:61208/api/4/mem - {"active": 5540470784, - "available": 11137699840, - "buffers": 226918400, - "cached": 6333566976, - "free": 11137699840, - "inactive": 3872489472, - "percent": 32.2, - "shared": 656637952, + {"active": 8055664640, + "available": 4539228160, + "buffers": 112582656, + "cached": 4634251264, + "free": 4539228160, + "inactive": 5664567296, + "percent": 72.4, + "shared": 794791936, "total": 16422486016, - "used": 5284786176} + "used": 11883257856} Fields descriptions: @@ -597,13 +558,13 @@ GET memswap Get plugin stats:: # curl http://localhost:61208/api/4/memswap - {"free": 4294963200, - "percent": 0.0, - "sin": 0, - "sout": 0, + {"free": 3367235584, + "percent": 21.6, + "sin": 12046336, + "sout": 929779712, "time_since_update": 1, "total": 4294963200, - "used": 0} + "used": 927727616} Fields descriptions: @@ -628,26 +589,15 @@ Get plugin stats:: # curl http://localhost:61208/api/4/network [{"alias": None, "bytes_all": 0, - "bytes_all_gauge": 422462144, + "bytes_all_gauge": 5602118637, "bytes_recv": 0, - "bytes_recv_gauge": 413272561, + "bytes_recv_gauge": 5324018799, "bytes_sent": 0, - "bytes_sent_gauge": 9189583, + "bytes_sent_gauge": 278099838, "interface_name": "wlp0s20f3", "key": "interface_name", "speed": 0, - "time_since_update": 0.24814295768737793}, - {"alias": None, - "bytes_all": 0, - "bytes_all_gauge": 18987, - "bytes_recv": 0, - "bytes_recv_gauge": 528, - "bytes_sent": 0, - "bytes_sent_gauge": 18459, - "interface_name": "vethfc47299", - "key": "interface_name", - "speed": 10485760000, - "time_since_update": 0.24814295768737793}] + "time_since_update": 0.4937009811401367}] Fields descriptions: @@ -669,22 +619,22 @@ Fields descriptions: Get a specific field:: # curl http://localhost:61208/api/4/network/interface_name - {"interface_name": ["wlp0s20f3", "vethfc47299"]} + {"interface_name": ["wlp0s20f3"]} Get a specific item when field matches the given value:: # curl http://localhost:61208/api/4/network/interface_name/wlp0s20f3 {"wlp0s20f3": [{"alias": None, "bytes_all": 0, - "bytes_all_gauge": 422462144, + "bytes_all_gauge": 5602118637, "bytes_recv": 0, - "bytes_recv_gauge": 413272561, + "bytes_recv_gauge": 5324018799, "bytes_sent": 0, - "bytes_sent_gauge": 9189583, + "bytes_sent_gauge": 278099838, "interface_name": "wlp0s20f3", "key": "interface_name", "speed": 0, - "time_since_update": 0.24814295768737793}]} + "time_since_update": 0.4937009811401367}]} GET now ------- @@ -692,7 +642,7 @@ GET now Get plugin stats:: # curl http://localhost:61208/api/4/now - {"custom": "2024-06-08 10:22:29 CEST", "iso": "2024-06-08T10:22:29+02:00"} + {"custom": "2024-06-29 09:51:57 CEST", "iso": "2024-06-29T09:51:57+02:00"} Fields descriptions: @@ -702,7 +652,7 @@ Fields descriptions: Get a specific field:: # curl http://localhost:61208/api/4/now/iso - {"iso": "2024-06-08T10:22:29+02:00"} + {"iso": "2024-06-29T09:51:57+02:00"} GET percpu ---------- @@ -713,7 +663,7 @@ Get plugin stats:: [{"cpu_number": 0, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 39.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", @@ -721,12 +671,12 @@ Get plugin stats:: "softirq": 0.0, "steal": 0.0, "system": 0.0, - "total": 100.0, + "total": 61.0, "user": 0.0}, {"cpu_number": 1, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 40.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", @@ -734,7 +684,7 @@ Get plugin stats:: "softirq": 0.0, "steal": 0.0, "system": 0.0, - "total": 100.0, + "total": 60.0, "user": 0.0}] Fields descriptions: @@ -769,7 +719,7 @@ Get plugin stats:: "port": 0, "refresh": 30, "rtt_warning": None, - "status": 0.005593, + "status": 0.00589, "timeout": 3}] Fields descriptions: @@ -797,7 +747,7 @@ Get a specific item when field matches the given value:: "port": 0, "refresh": 30, "rtt_warning": None, - "status": 0.005593, + "status": 0.00589, "timeout": 3}]} GET processcount @@ -806,7 +756,7 @@ GET processcount Get plugin stats:: # curl http://localhost:61208/api/4/processcount - {"pid_max": 0, "running": 0, "sleeping": 279, "thread": 1568, "total": 410} + {"pid_max": 0, "running": 1, "sleeping": 280, "thread": 1620, "total": 419} Fields descriptions: @@ -819,7 +769,7 @@ Fields descriptions: Get a specific field:: # curl http://localhost:61208/api/4/processcount/total - {"total": 410} + {"total": 419} GET processlist --------------- @@ -827,7 +777,100 @@ GET processlist Get plugin stats:: # curl http://localhost:61208/api/4/processlist - [] + [{"cmdline": ["/snap/firefox/4336/usr/lib/firefox/firefox", + "-contentproc", + "-childID", + "2", + "-isForBrowser", + "-prefsLen", + "28296", + "-prefMapSize", + "244444", + "-jsInitLen", + "231800", + "-parentBuildID", + "20240527194810", + "-greomni", + "/snap/firefox/4336/usr/lib/firefox/omni.ja", + "-appomni", + "/snap/firefox/4336/usr/lib/firefox/browser/omni.ja", + "-appDir", + "/snap/firefox/4336/usr/lib/firefox/browser", + "{01aadc3d-85fc-4851-9ca1-a43a1e81c3fa}", + "4591", + "true", + "tab"], + "cpu_percent": 0.0, + "cpu_times": {"children_system": 0.0, + "children_user": 0.0, + "iowait": 0.0, + "system": 123.52, + "user": 2733.04}, + "gids": {"effective": 1000, "real": 1000, "saved": 1000}, + "io_counters": [8411136, 0, 0, 0, 0], + "key": "pid", + "memory_info": {"data": 3639250944, + "dirty": 0, + "lib": 0, + "rss": 3594973184, + "shared": 128897024, + "text": 987136, + "vms": 6192345088}, + "memory_percent": 21.89055408844624, + "name": "Isolated Web Co", + "nice": 0, + "num_threads": 28, + "pid": 4848, + "status": "S", + "time_since_update": 1, + "username": "nicolargo"}, + {"cmdline": ["/snap/firefox/4336/usr/lib/firefox/firefox", + "-contentproc", + "-childID", + "3", + "-isForBrowser", + "-prefsLen", + "28296", + "-prefMapSize", + "244444", + "-jsInitLen", + "231800", + "-parentBuildID", + "20240527194810", + "-greomni", + "/snap/firefox/4336/usr/lib/firefox/omni.ja", + "-appomni", + "/snap/firefox/4336/usr/lib/firefox/browser/omni.ja", + "-appDir", + "/snap/firefox/4336/usr/lib/firefox/browser", + "{0ae685c6-7105-4724-886c-98d4a4a9a4f8}", + "4591", + "true", + "tab"], + "cpu_percent": 0.0, + "cpu_times": {"children_system": 0.0, + "children_user": 0.0, + "iowait": 0.0, + "system": 52.27, + "user": 492.22}, + "gids": {"effective": 1000, "real": 1000, "saved": 1000}, + "io_counters": [2974720, 0, 0, 0, 0], + "key": "pid", + "memory_info": {"data": 1949392896, + "dirty": 0, + "lib": 0, + "rss": 1926062080, + "shared": 121397248, + "text": 987136, + "vms": 4444565504}, + "memory_percent": 11.72820045712621, + "name": "Isolated Web Co", + "nice": 0, + "num_threads": 28, + "pid": 4852, + "status": "S", + "time_since_update": 1, + "username": "nicolargo"}] Fields descriptions: @@ -851,7 +894,7 @@ GET psutilversion Get plugin stats:: # curl http://localhost:61208/api/4/psutilversion - "5.9.8" + "6.0.0" GET quicklook ------------- @@ -859,18 +902,18 @@ GET quicklook Get plugin stats:: # curl http://localhost:61208/api/4/quicklook - {"cpu": 16.7, + {"cpu": 6.7, "cpu_hz": 4475000000.0, - "cpu_hz_current": 1230624312.5, + "cpu_hz_current": 676840312.5, "cpu_log_core": 16, "cpu_name": "13th Gen Intel(R) Core(TM) i7-13620H", "cpu_phys_core": 10, - "load": 3.4, - "mem": 32.1, + "load": 5.8, + "mem": 72.4, "percpu": [{"cpu_number": 0, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 39.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", @@ -878,12 +921,12 @@ Get plugin stats:: "softirq": 0.0, "steal": 0.0, "system": 0.0, - "total": 100.0, + "total": 61.0, "user": 0.0}, {"cpu_number": 1, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 40.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", @@ -891,12 +934,12 @@ Get plugin stats:: "softirq": 0.0, "steal": 0.0, "system": 0.0, - "total": 100.0, + "total": 60.0, "user": 0.0}, {"cpu_number": 2, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 39.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", @@ -904,12 +947,12 @@ Get plugin stats:: "softirq": 0.0, "steal": 0.0, "system": 0.0, - "total": 100.0, + "total": 61.0, "user": 0.0}, {"cpu_number": 3, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 40.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", @@ -917,25 +960,25 @@ Get plugin stats:: "softirq": 0.0, "steal": 0.0, "system": 0.0, - "total": 100.0, + "total": 60.0, "user": 0.0}, {"cpu_number": 4, "guest": 0.0, "guest_nice": 0.0, "idle": 0.0, - "iowait": 0.0, + "iowait": 1.0, "irq": 0.0, "key": "cpu_number", "nice": 0.0, "softirq": 0.0, "steal": 0.0, - "system": 0.0, + "system": 4.0, "total": 100.0, - "user": 0.0}, + "user": 35.0}, {"cpu_number": 5, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 40.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", @@ -943,12 +986,12 @@ Get plugin stats:: "softirq": 0.0, "steal": 0.0, "system": 0.0, - "total": 100.0, - "user": 0.0}, + "total": 60.0, + "user": 1.0}, {"cpu_number": 6, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 40.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", @@ -956,12 +999,12 @@ Get plugin stats:: "softirq": 0.0, "steal": 0.0, "system": 0.0, - "total": 100.0, + "total": 60.0, "user": 0.0}, {"cpu_number": 7, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 40.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", @@ -969,12 +1012,12 @@ Get plugin stats:: "softirq": 0.0, "steal": 0.0, "system": 0.0, - "total": 100.0, + "total": 60.0, "user": 0.0}, {"cpu_number": 8, "guest": 0.0, "guest_nice": 0.0, - "idle": 1.0, + "idle": 40.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", @@ -982,25 +1025,25 @@ Get plugin stats:: "softirq": 0.0, "steal": 0.0, "system": 0.0, - "total": 99.0, - "user": 0.0}, + "total": 60.0, + "user": 1.0}, {"cpu_number": 9, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 39.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", "nice": 0.0, "softirq": 0.0, "steal": 0.0, - "system": 0.0, - "total": 100.0, + "system": 1.0, + "total": 61.0, "user": 0.0}, {"cpu_number": 10, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 40.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", @@ -1008,12 +1051,12 @@ Get plugin stats:: "softirq": 0.0, "steal": 0.0, "system": 0.0, - "total": 100.0, + "total": 60.0, "user": 0.0}, {"cpu_number": 11, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 40.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", @@ -1021,12 +1064,12 @@ Get plugin stats:: "softirq": 0.0, "steal": 0.0, "system": 0.0, - "total": 100.0, + "total": 60.0, "user": 0.0}, {"cpu_number": 12, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 40.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", @@ -1034,25 +1077,25 @@ Get plugin stats:: "softirq": 0.0, "steal": 0.0, "system": 0.0, - "total": 100.0, + "total": 60.0, "user": 0.0}, {"cpu_number": 13, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 40.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", "nice": 0.0, "softirq": 0.0, "steal": 0.0, - "system": 0.0, - "total": 100.0, + "system": 1.0, + "total": 60.0, "user": 0.0}, {"cpu_number": 14, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 40.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", @@ -1060,12 +1103,12 @@ Get plugin stats:: "softirq": 0.0, "steal": 0.0, "system": 0.0, - "total": 100.0, + "total": 60.0, "user": 0.0}, {"cpu_number": 15, "guest": 0.0, "guest_nice": 0.0, - "idle": 0.0, + "idle": 40.0, "iowait": 0.0, "irq": 0.0, "key": "cpu_number", @@ -1073,9 +1116,9 @@ Get plugin stats:: "softirq": 0.0, "steal": 0.0, "system": 0.0, - "total": 100.0, + "total": 60.0, "user": 0.0}], - "swap": 0.0} + "swap": 21.6} Fields descriptions: @@ -1113,14 +1156,14 @@ Get plugin stats:: "label": "Ambient", "type": "temperature_core", "unit": "C", - "value": 36, + "value": 35, "warning": 0}, {"critical": None, "key": "label", "label": "Ambient 3", "type": "temperature_core", "unit": "C", - "value": 29, + "value": 31, "warning": 0}] Fields descriptions: @@ -1181,7 +1224,7 @@ Get a specific item when field matches the given value:: "label": "Ambient", "type": "temperature_core", "unit": "C", - "value": 36, + "value": 35, "warning": 0}]} GET smart @@ -1225,7 +1268,7 @@ GET uptime Get plugin stats:: # curl http://localhost:61208/api/4/uptime - "0:14:44" + "19 days, 16:54:43" GET version ----------- @@ -1233,7 +1276,7 @@ GET version Get plugin stats:: # curl http://localhost:61208/api/4/version - "4.0.8" + "4.1.0" GET wifi -------- @@ -1242,8 +1285,8 @@ Get plugin stats:: # curl http://localhost:61208/api/4/wifi [{"key": "ssid", - "quality_level": -66.0, - "quality_link": 44.0, + "quality_level": -61.0, + "quality_link": 49.0, "ssid": "wlp0s20f3"}] Get a specific field:: @@ -1255,8 +1298,8 @@ Get a specific item when field matches the given value:: # curl http://localhost:61208/api/4/wifi/ssid/wlp0s20f3 {"wlp0s20f3": [{"key": "ssid", - "quality_level": -66.0, - "quality_link": 44.0, + "quality_level": -61.0, + "quality_link": 49.0, "ssid": "wlp0s20f3"}]} GET all stats @@ -1301,34 +1344,34 @@ GET stats history History of a plugin:: # curl http://localhost:61208/api/4/cpu/history - {"system": [["2024-06-08T10:22:30.631056", 0.0], - ["2024-06-08T10:22:31.667772", 1.0], - ["2024-06-08T10:22:32.723737", 1.0]], - "user": [["2024-06-08T10:22:30.631046", 1.0], - ["2024-06-08T10:22:31.667765", 1.0], - ["2024-06-08T10:22:32.723728", 1.0]]} + {"system": [["2024-06-29T09:51:58.684649", 3.2], + ["2024-06-29T09:51:59.762140", 0.6], + ["2024-06-29T09:52:00.774689", 0.6]], + "user": [["2024-06-29T09:51:58.684643", 11.4], + ["2024-06-29T09:51:59.762137", 0.7], + ["2024-06-29T09:52:00.774685", 0.7]]} Limit history to last 2 values:: # curl http://localhost:61208/api/4/cpu/history/2 - {"system": [["2024-06-08T10:22:31.667772", 1.0], - ["2024-06-08T10:22:32.723737", 1.0]], - "user": [["2024-06-08T10:22:31.667765", 1.0], - ["2024-06-08T10:22:32.723728", 1.0]]} + {"system": [["2024-06-29T09:51:59.762140", 0.6], + ["2024-06-29T09:52:00.774689", 0.6]], + "user": [["2024-06-29T09:51:59.762137", 0.7], + ["2024-06-29T09:52:00.774685", 0.7]]} History for a specific field:: # curl http://localhost:61208/api/4/cpu/system/history - {"system": [["2024-06-08T10:22:29.515717", 0.0], - ["2024-06-08T10:22:30.631056", 0.0], - ["2024-06-08T10:22:31.667772", 1.0], - ["2024-06-08T10:22:32.723737", 1.0]]} + {"system": [["2024-06-29T09:51:57.513763", 3.2], + ["2024-06-29T09:51:58.684649", 3.2], + ["2024-06-29T09:51:59.762140", 0.6], + ["2024-06-29T09:52:00.774689", 0.6]]} Limit history for a specific field to last 2 values:: # curl http://localhost:61208/api/4/cpu/system/history - {"system": [["2024-06-08T10:22:31.667772", 1.0], - ["2024-06-08T10:22:32.723737", 1.0]]} + {"system": [["2024-06-29T09:51:59.762140", 0.6], + ["2024-06-29T09:52:00.774689", 0.6]]} GET limits (used for thresholds) -------------------------------- @@ -1413,6 +1456,8 @@ All limits/thresholds:: "network": {"history_size": 1200.0, "network_disable": ["False"], "network_hide": ["docker.*", "lo"], + "network_hide_no_ip": ["True"], + "network_hide_no_up": ["True"], "network_rx_careful": 70.0, "network_rx_critical": 90.0, "network_rx_warning": 80.0, diff --git a/docs/man/glances.1 b/docs/man/glances.1 index 5927396d..c43028f7 100644 --- a/docs/man/glances.1 +++ b/docs/man/glances.1 @@ -27,7 +27,7 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "GLANCES" "1" "Jun 08, 2024" "4.0.8" "Glances" +.TH "GLANCES" "1" "Jun 29, 2024" "4.1.0" "Glances" .SH NAME glances \- An eye on your system .SH SYNOPSIS diff --git a/glances/__init__.py b/glances/__init__.py index b1a0ec31..796a1119 100644 --- a/glances/__init__.py +++ b/glances/__init__.py @@ -19,7 +19,7 @@ import tracemalloc # Global name # Version should start and end with a numerical char # See https://packaging.python.org/specifications/core-metadata/#version -__version__ = '4.0.8' +__version__ = '4.1.0' __apiversion__ = '4' __author__ = 'Nicolas Hennion ' __license__ = 'LGPLv3' diff --git a/glances/client_browser.py b/glances/client_browser.py index 8e0ec721..12a52680 100644 --- a/glances/client_browser.py +++ b/glances/client_browser.py @@ -95,12 +95,16 @@ class GlancesClientBrowser: # Mandatory stats try: # CPU% - cpu_percent = 100 - orjson.loads(s.getPlugin('cpu'))['idle'] - server['cpu_percent'] = f'{cpu_percent:.1f}' + # logger.info(f"CPU stats {s.getPlugin('cpu')}") + # logger.info(f"CPU views {s.getPluginView('cpu')}") + server['cpu_percent'] = orjson.loads(s.getPlugin('cpu'))['total'] + server['cpu_percent_decoration'] = orjson.loads(s.getPluginView('cpu'))['total']['decoration'] # MEM% server['mem_percent'] = orjson.loads(s.getPlugin('mem'))['percent'] + server['mem_percent_decoration'] = orjson.loads(s.getPluginView('mem'))['percent']['decoration'] # OS (Human Readable name) server['hr_name'] = orjson.loads(s.getPlugin('system'))['hr_name'] + server['hr_name_decoration'] = 'DEFAULT' except (OSError, Fault, KeyError) as e: logger.debug(f"Error while grabbing stats form server ({e})") server['status'] = 'OFFLINE' @@ -120,8 +124,8 @@ class GlancesClientBrowser: # Optional stats (load is not available on Windows OS) try: # LOAD - load_min5 = orjson.loads(s.getPlugin('load'))['min5'] - server['load_min5'] = f'{load_min5:.2f}' + server['load_min5'] = round(orjson.loads(s.getPlugin('load'))['min5'], 1) + server['load_min5_decoration'] = orjson.loads(s.getPluginView('load'))['min5']['decoration'] except Exception as e: logger.warning(f"Error while grabbing stats form server ({e})") diff --git a/glances/cpu_percent.py b/glances/cpu_percent.py index 690f9fd1..baa3b359 100644 --- a/glances/cpu_percent.py +++ b/glances/cpu_percent.py @@ -17,36 +17,30 @@ from glances.timer import Timer class CpuPercent: """Get and store the CPU percent.""" - def __init__(self, cached_timer_cpu=3): - self.cpu_info = {'cpu_name': None, 'cpu_hz_current': None, 'cpu_hz': None} - self.cpu_percent = 0 - self.percpu_percent = [] - - # Get CPU name - self.cpu_info['cpu_name'] = self.__get_cpu_name() - + def __init__(self, cached_timer_cpu=2): # cached_timer_cpu is the minimum time interval between stats updates # since last update is passed (will retrieve old cached info instead) self.cached_timer_cpu = cached_timer_cpu - self.timer_cpu = Timer(0) - self.timer_percpu = Timer(0) - # psutil.cpu_freq() consumes lots of CPU - # So refresh the stats every refresh*2 (6 seconds) + # So refresh CPU frequency stats every refresh * 2 self.cached_timer_cpu_info = cached_timer_cpu * 2 + + # Get CPU name self.timer_cpu_info = Timer(0) + self.cpu_info = {'cpu_name': self.__get_cpu_name(), 'cpu_hz_current': None, 'cpu_hz': None} + + # Warning from PsUtil documentation + # The first time this function is called with interval = 0.0 or None + # it will return a meaningless 0.0 value which you are supposed to ignore. + self.timer_cpu = Timer(0) + self.cpu_percent = self.get_cpu() + self.timer_percpu = Timer(0) + self.percpu_percent = self.get_percpu() def get_key(self): """Return the key of the per CPU list.""" return 'cpu_number' - def get(self, percpu=False): - """Update and/or return the CPU using the psutil library. - If percpu, return the percpu stats""" - if percpu: - return self.__get_percpu() - return self.__get_cpu() - def get_info(self): """Get additional information about the CPU""" # Never update more than 1 time per cached_timer_cpu_info @@ -71,7 +65,7 @@ class CpuPercent: def __get_cpu_name(self): # Get the CPU name once from the /proc/cpuinfo file - # Read the first line with the "model name" + # Read the first line with the "model name" ("Model" for Raspberry Pi) ret = None try: cpuinfo_file = open('/proc/cpuinfo').readlines() @@ -79,26 +73,31 @@ class CpuPercent: pass else: for line in cpuinfo_file: - if line.startswith('model name'): + if line.startswith('model name') or line.startswith('Model') or line.startswith('cpu model'): ret = line.split(':')[1].strip() break return ret if ret else 'CPU' - def __get_cpu(self): + def get_cpu(self): """Update and/or return the CPU using the psutil library.""" # Never update more than 1 time per cached_timer_cpu if self.timer_cpu.finished(): - self.cpu_percent = psutil.cpu_percent(interval=0.0) # Reset timer for cache self.timer_cpu.reset(duration=self.cached_timer_cpu) + # Update the stats + self.cpu_percent = psutil.cpu_percent(interval=0.0) return self.cpu_percent - def __get_percpu(self): + def get_percpu(self): """Update and/or return the per CPU list using the psutil library.""" # Never update more than 1 time per cached_timer_cpu if self.timer_percpu.finished(): - self.percpu_percent = [] - for cpu_number, cputimes in enumerate(psutil.cpu_times_percent(interval=0.0, percpu=True)): + # Reset timer for cache + self.timer_percpu.reset(duration=self.cached_timer_cpu) + # Get stats + percpu_percent = [] + psutil_percpu = enumerate(psutil.cpu_times_percent(interval=0.0, percpu=True)) + for cpu_number, cputimes in psutil_percpu: cpu = { 'key': self.get_key(), 'cpu_number': cpu_number, @@ -123,9 +122,9 @@ class CpuPercent: if hasattr(cputimes, 'guest_nice'): cpu['guest_nice'] = cputimes.guest_nice # Append new CPU to the list - self.percpu_percent.append(cpu) - # Reset timer for cache - self.timer_percpu.reset(duration=self.cached_timer_cpu) + percpu_percent.append(cpu) + # Update stats + self.percpu_percent = percpu_percent return self.percpu_percent diff --git a/glances/exports/export.py b/glances/exports/export.py index 0241e1b8..dd6fa346 100644 --- a/glances/exports/export.py +++ b/glances/exports/export.py @@ -196,11 +196,10 @@ class GlancesExport: for key, value in sorted(iteritems(stats)): if isinstance(value, bool): value = json_dumps(value) + if isinstance(value, list): - try: - value = value[0] - except IndexError: - value = '' + value = ' '.join([str(v) for v in value]) + if isinstance(value, dict): item_names, item_values = self.build_export(value) item_names = [pre_key + key.lower() + str(i) for i in item_names] diff --git a/glances/exports/glances_graph/__init__.py b/glances/exports/glances_graph/__init__.py index 542be577..4138c014 100644 --- a/glances/exports/glances_graph/__init__.py +++ b/glances/exports/glances_graph/__init__.py @@ -34,10 +34,12 @@ class Export(GlancesExport): # Manage options (command line arguments overwrite configuration file) self.path = args.export_graph_path or self.path - self.generate_every = int(getattr(self, 'generate_every', 0)) - self.width = int(getattr(self, 'width', 800)) - self.height = int(getattr(self, 'height', 600)) - self.style = getattr(pygal.style, getattr(self, 'style', 'DarkStyle'), pygal.style.DarkStyle) + self.generate_every = int(getattr(self, 'generate_every', 0) or 0) + self.width = int(getattr(self, 'width', 800) or 800) + self.height = int(getattr(self, 'height', 600) or 600) + self.style = ( + getattr(pygal.style, getattr(self, 'style', 'DarkStyle'), pygal.style.DarkStyle) or pygal.style.DarkStyle + ) # Create export folder try: diff --git a/glances/outputs/glances_curses.py b/glances/outputs/glances_curses.py index 416bb3a2..1db8d752 100644 --- a/glances/outputs/glances_curses.py +++ b/glances/outputs/glances_curses.py @@ -162,7 +162,7 @@ class _GlancesCurses: self._init_cursor() # Init the colors - self._init_colors() + self.colors_list = build_colors_list(args) # Init main window self.term_window = self.screen.subwin(0, 0) @@ -195,8 +195,10 @@ class _GlancesCurses: """Load the outputs section of the configuration file.""" if config is not None and config.has_section('outputs'): logger.debug('Read the outputs section in the configuration file') - # Separator ? - self.args.enable_separator = config.get_bool_value('outputs', 'separator', default=True) + # Separator + self.args.enable_separator = config.get_bool_value( + 'outputs', 'separator', default=self.args.enable_separator + ) # Set the left sidebar list self._left_sidebar = config.get_list_value('outputs', 'left_menu', default=self._left_sidebar) @@ -214,133 +216,6 @@ class _GlancesCurses: curses.cbreak() self.set_cursor(0) - def _init_colors(self): - """Init the Curses color layout.""" - - # Set curses options - try: - if hasattr(curses, 'start_color'): - curses.start_color() - logger.debug(f'Curses interface compatible with {curses.COLORS} colors') - if hasattr(curses, 'use_default_colors'): - curses.use_default_colors() - except Exception as e: - logger.warning(f'Error initializing terminal color ({e})') - - # Init colors - if self.args.disable_bold: - A_BOLD = 0 - self.args.disable_bg = True - else: - A_BOLD = curses.A_BOLD - - self.title_color = A_BOLD - self.title_underline_color = A_BOLD | curses.A_UNDERLINE - self.help_color = A_BOLD - - if curses.has_colors(): - # The screen is compatible with a colored design - # ex: export TERM=xterm-256color - # export TERM=xterm-color - - curses.init_pair(1, -1, -1) - if self.args.disable_bg: - curses.init_pair(2, curses.COLOR_RED, -1) - curses.init_pair(3, curses.COLOR_GREEN, -1) - curses.init_pair(5, curses.COLOR_MAGENTA, -1) - else: - curses.init_pair(2, -1, curses.COLOR_RED) - curses.init_pair(3, -1, curses.COLOR_GREEN) - curses.init_pair(5, -1, curses.COLOR_MAGENTA) - curses.init_pair(4, curses.COLOR_BLUE, -1) - curses.init_pair(6, curses.COLOR_RED, -1) - curses.init_pair(7, curses.COLOR_GREEN, -1) - curses.init_pair(8, curses.COLOR_MAGENTA, -1) - - # Colors text styles - self.no_color = curses.color_pair(1) - self.default_color = curses.color_pair(3) | A_BOLD - self.nice_color = curses.color_pair(8) - self.cpu_time_color = curses.color_pair(8) - self.ifCAREFUL_color = curses.color_pair(4) | A_BOLD - self.ifWARNING_color = curses.color_pair(5) | A_BOLD - self.ifCRITICAL_color = curses.color_pair(2) | A_BOLD - self.default_color2 = curses.color_pair(7) - self.ifCAREFUL_color2 = curses.color_pair(4) - self.ifWARNING_color2 = curses.color_pair(8) | A_BOLD - self.ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD - self.ifINFO_color = curses.color_pair(4) - self.filter_color = A_BOLD - self.selected_color = A_BOLD - self.separator = curses.color_pair(1) - - if curses.COLORS > 8: - # ex: export TERM=xterm-256color - colors_list = [curses.COLOR_CYAN, curses.COLOR_YELLOW] - for i in range(0, 3): - try: - curses.init_pair(i + 9, colors_list[i], -1) - except Exception: - curses.init_pair(i + 9, -1, -1) - self.filter_color = curses.color_pair(9) | A_BOLD - self.selected_color = curses.color_pair(10) | A_BOLD - # Define separator line style - try: - curses.init_color(11, 500, 500, 500) - curses.init_pair(11, curses.COLOR_BLACK, -1) - self.separator = curses.color_pair(11) - except Exception: - # Catch exception in TMUX - pass - else: - # The screen is NOT compatible with a colored design - # switch to B&W text styles - # ex: export TERM=xterm-mono - self.no_color = -1 - self.default_color = -1 - self.nice_color = A_BOLD - self.cpu_time_color = A_BOLD - self.ifCAREFUL_color = A_BOLD - self.ifWARNING_color = curses.A_UNDERLINE - self.ifCRITICAL_color = curses.A_REVERSE - self.default_color2 = -1 - self.ifCAREFUL_color2 = A_BOLD - self.ifWARNING_color2 = curses.A_UNDERLINE - self.ifCRITICAL_color2 = curses.A_REVERSE - self.ifINFO_color = A_BOLD - self.filter_color = A_BOLD - self.selected_color = A_BOLD - self.separator = -1 - - # Define the colors list (hash table) for stats - self.colors_list = { - 'DEFAULT': self.no_color, - 'UNDERLINE': curses.A_UNDERLINE, - 'BOLD': A_BOLD, - 'SORT': curses.A_UNDERLINE | A_BOLD, - 'OK': self.default_color2, - 'MAX': self.default_color2 | A_BOLD, - 'FILTER': self.filter_color, - 'TITLE': self.title_color, - 'PROCESS': self.default_color2, - 'PROCESS_SELECTED': self.default_color2 | curses.A_UNDERLINE, - 'STATUS': self.default_color2, - 'NICE': self.nice_color, - 'CPU_TIME': self.cpu_time_color, - 'CAREFUL': self.ifCAREFUL_color2, - 'WARNING': self.ifWARNING_color2, - 'CRITICAL': self.ifCRITICAL_color2, - 'OK_LOG': self.default_color, - 'CAREFUL_LOG': self.ifCAREFUL_color, - 'WARNING_LOG': self.ifWARNING_color, - 'CRITICAL_LOG': self.ifCRITICAL_color, - 'PASSWORD': curses.A_PROTECT, - 'SELECTED': self.selected_color, - 'INFO': self.ifINFO_color, - 'ERROR': self.selected_color, - 'SEPARATOR': self.separator, - } - def set_cursor(self, value): """Configure the curse cursor appearance. @@ -495,7 +370,7 @@ class _GlancesCurses: logger.info(f"Stop Glances (keypressed: {self.pressedkey})") def _handle_refresh(self): - pass + glances_processes.reset_internal_cache() def loop_position(self): """Return the current sort in the loop""" @@ -570,13 +445,14 @@ class _GlancesCurses: self.new_line() self.line -= 1 line_width = self.term_window.getmaxyx()[1] - self.column - self.term_window.addnstr( - self.line, - self.column, - unicode_message('MEDIUM_LINE', self.args) * line_width, - line_width, - self.colors_list[color], - ) + if self.line >= 0: + self.term_window.addnstr( + self.line, + self.column, + unicode_message('MEDIUM_LINE', self.args) * line_width, + line_width, + self.colors_list[color], + ) def __get_stat_display(self, stats, layer): """Return a dict of dict with all the stats display. @@ -1300,3 +1176,128 @@ class GlancesTextboxYesNo(Textbox): def do_command(self, ch): return super().do_command(ch) + + +def build_colors_list(args): + """Init the Curses color layout.""" + # Set curses options + try: + if hasattr(curses, 'start_color'): + curses.start_color() + logger.debug(f'Curses interface compatible with {curses.COLORS} colors') + if hasattr(curses, 'use_default_colors'): + curses.use_default_colors() + except Exception as e: + logger.warning(f'Error initializing terminal color ({e})') + + # Init colors + if args.disable_bold: + A_BOLD = 0 + args.disable_bg = True + else: + A_BOLD = curses.A_BOLD + + title_color = A_BOLD + + if curses.has_colors(): + # The screen is compatible with a colored design + # ex: export TERM=xterm-256color + # export TERM=xterm-color + + curses.init_pair(1, -1, -1) + if args.disable_bg: + curses.init_pair(2, curses.COLOR_RED, -1) + curses.init_pair(3, curses.COLOR_GREEN, -1) + curses.init_pair(5, curses.COLOR_MAGENTA, -1) + else: + curses.init_pair(2, -1, curses.COLOR_RED) + curses.init_pair(3, -1, curses.COLOR_GREEN) + curses.init_pair(5, -1, curses.COLOR_MAGENTA) + curses.init_pair(4, curses.COLOR_BLUE, -1) + curses.init_pair(6, curses.COLOR_RED, -1) + curses.init_pair(7, curses.COLOR_GREEN, -1) + curses.init_pair(8, curses.COLOR_MAGENTA, -1) + + # Colors text styles + no_color = curses.color_pair(1) + default_color = curses.color_pair(3) | A_BOLD + nice_color = curses.color_pair(8) + cpu_time_color = curses.color_pair(8) + ifCAREFUL_color = curses.color_pair(4) | A_BOLD + ifWARNING_color = curses.color_pair(5) | A_BOLD + ifCRITICAL_color = curses.color_pair(2) | A_BOLD + default_color2 = curses.color_pair(7) + ifCAREFUL_color2 = curses.color_pair(4) + ifWARNING_color2 = curses.color_pair(8) | A_BOLD + ifCRITICAL_color2 = curses.color_pair(6) | A_BOLD + ifINFO_color = curses.color_pair(4) + filter_color = A_BOLD + selected_color = A_BOLD + separator = curses.color_pair(1) + + if curses.COLORS > 8: + # ex: export TERM=xterm-256color + colors_list = [curses.COLOR_CYAN, curses.COLOR_YELLOW] + for i in range(0, 3): + try: + curses.init_pair(i + 9, colors_list[i], -1) + except Exception: + curses.init_pair(i + 9, -1, -1) + filter_color = curses.color_pair(9) | A_BOLD + selected_color = curses.color_pair(10) | A_BOLD + # Define separator line style + try: + curses.init_color(11, 500, 500, 500) + curses.init_pair(11, curses.COLOR_BLACK, -1) + separator = curses.color_pair(11) + except Exception: + # Catch exception in TMUX + pass + else: + # The screen is NOT compatible with a colored design + # switch to B&W text styles + # ex: export TERM=xterm-mono + no_color = -1 + default_color = -1 + nice_color = A_BOLD + cpu_time_color = A_BOLD + ifCAREFUL_color = A_BOLD + ifWARNING_color = curses.A_UNDERLINE + ifCRITICAL_color = curses.A_REVERSE + default_color2 = -1 + ifCAREFUL_color2 = A_BOLD + ifWARNING_color2 = curses.A_UNDERLINE + ifCRITICAL_color2 = curses.A_REVERSE + ifINFO_color = A_BOLD + filter_color = A_BOLD + selected_color = A_BOLD + separator = -1 + + # Define the colors list (hash table) for stats + return { + 'DEFAULT': no_color, + 'UNDERLINE': curses.A_UNDERLINE, + 'BOLD': A_BOLD, + 'SORT': curses.A_UNDERLINE | A_BOLD, + 'OK': default_color2, + 'MAX': default_color2 | A_BOLD, + 'FILTER': filter_color, + 'TITLE': title_color, + 'PROCESS': default_color2, + 'PROCESS_SELECTED': default_color2 | curses.A_UNDERLINE, + 'STATUS': default_color2, + 'NICE': nice_color, + 'CPU_TIME': cpu_time_color, + 'CAREFUL': ifCAREFUL_color2, + 'WARNING': ifWARNING_color2, + 'CRITICAL': ifCRITICAL_color2, + 'OK_LOG': default_color, + 'CAREFUL_LOG': ifCAREFUL_color, + 'WARNING_LOG': ifWARNING_color, + 'CRITICAL_LOG': ifCRITICAL_color, + 'PASSWORD': curses.A_PROTECT, + 'SELECTED': selected_color, + 'INFO': ifINFO_color, + 'ERROR': selected_color, + 'SEPARATOR': separator, + } diff --git a/glances/outputs/glances_curses_browser.py b/glances/outputs/glances_curses_browser.py index 858fd5fc..20d0972f 100644 --- a/glances/outputs/glances_curses_browser.py +++ b/glances/outputs/glances_curses_browser.py @@ -24,11 +24,11 @@ class GlancesCursesBrowser(_GlancesCurses): super().__init__(args=args) _colors_list = { - 'UNKNOWN': self.no_color, - 'SNMP': self.default_color2, - 'ONLINE': self.default_color2, - 'OFFLINE': self.ifCRITICAL_color2, - 'PROTECTED': self.ifWARNING_color2, + 'UNKNOWN': self.colors_list['DEFAULT'], + 'SNMP': self.colors_list['OK'], + 'ONLINE': self.colors_list['OK'], + 'OFFLINE': self.colors_list['CRITICAL'], + 'PROTECTED': self.colors_list['WARNING'], } self.colors_list.update(_colors_list) @@ -299,13 +299,11 @@ class GlancesCursesBrowser(_GlancesCurses): # Item description: [stats_id, column name, column size] column_def = [ ['name', 'Name', 16], - ['alias', None, None], ['load_min5', 'LOAD', 6], ['cpu_percent', 'CPU%', 5], ['mem_percent', 'MEM%', 5], ['status', 'STATUS', 9], ['ip', 'IP', 15], - # ['port', 'PORT', 5], ['hr_name', 'OS', 16], ] y = 2 @@ -331,24 +329,10 @@ class GlancesCursesBrowser(_GlancesCurses): # Display table line = 0 - for v in current_page: + for server_stat in current_page: # Limit the number of displayed server (see issue #1256) if line >= stats_max: continue - # Get server stats - server_stat = {} - for c in column_def: - try: - server_stat[c[0]] = v[c[0]] - except KeyError as e: - logger.debug(f"Cannot grab stats {c[0]} from server (KeyError: {e})") - server_stat[c[0]] = '?' - # Display alias instead of name - try: - if c[0] == 'alias' and v[c[0]] is not None: - server_stat['name'] = v[c[0]] - except KeyError: - pass # Display line for server stats cpt = 0 @@ -362,9 +346,20 @@ class GlancesCursesBrowser(_GlancesCurses): # Display the line xc += 2 for c in column_def: - if xc < screen_x and y < screen_y and c[1] is not None: + if xc < screen_x and y < screen_y: # Display server stats - self.term_window.addnstr(y, xc, format(server_stat[c[0]]), c[2], self.colors_list[v['status']]) + value = format(server_stat.get(c[0], '?')) + if c[0] == 'name' and 'alias' in server_stat: + value = server_stat['alias'] + decoration = self.colors_list.get( + server_stat[c[0] + '_decoration'].replace('_LOG', '') + if c[0] + '_decoration' in server_stat + else self.colors_list[server_stat['status']], + self.colors_list['DEFAULT'], + ) + if c[0] == 'status': + decoration = self.colors_list[server_stat['status']] + self.term_window.addnstr(y, xc, value, c[2], decoration) xc += c[2] + self.space_between_column cpt += 1 # Next line, next server... diff --git a/glances/outputs/glances_restful_api.py b/glances/outputs/glances_restful_api.py index 61f5fa9d..c7f0490a 100644 --- a/glances/outputs/glances_restful_api.py +++ b/glances/outputs/glances_restful_api.py @@ -185,6 +185,7 @@ class GlancesRestfulApi: router.add_api_route( f'/api/{self.API_VERSION}/status', status_code=status.HTTP_200_OK, + methods=['HEAD', 'GET'], response_class=ORJSONResponse, endpoint=self._api_status, ) @@ -262,7 +263,7 @@ class GlancesRestfulApi: endpoint=self._api_item_unit, ) router.add_api_route( - f'/api/{self.API_VERSION}/{{plugin}}/{{item}}/{{value}}', + f'/api/{self.API_VERSION}/{{plugin}}/{{item}}/{{value:path}}', response_class=ORJSONResponse, endpoint=self._api_value, ) diff --git a/glances/outputs/static/package-lock.json b/glances/outputs/static/package-lock.json index 7963c978..9fb82999 100644 --- a/glances/outputs/static/package-lock.json +++ b/glances/outputs/static/package-lock.json @@ -1056,12 +1056,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2300,9 +2300,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -5800,15 +5800,6 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -5816,9 +5807,9 @@ "dev": true }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "engines": { "node": ">=10.0.0" @@ -6736,12 +6727,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "browserslist": { @@ -7663,9 +7654,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -10151,11 +10142,6 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, - "word-wrap": { - "version": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -10163,9 +10149,9 @@ "dev": true }, "ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, "requires": {} }, diff --git a/glances/plugins/containers/engines/docker.py b/glances/plugins/containers/engines/docker.py index 066e83a8..81e94174 100644 --- a/glances/plugins/containers/engines/docker.py +++ b/glances/plugins/containers/engines/docker.py @@ -9,10 +9,11 @@ """Docker Extension unit for Glances' Containers plugin.""" import time +from typing import Any, Dict, List, Optional, Tuple from glances.globals import iterkeys, itervalues, nativestr, pretty_date, replace_special_chars from glances.logger import logger -from glances.plugins.containers.stats_streamer import StatsStreamer +from glances.plugins.containers.stats_streamer import ThreadedIterableStreamer # Docker-py library (optional and Linux-only) # https://github.com/docker/docker-py @@ -43,7 +44,7 @@ class DockerStatsFetcher: # Threaded Streamer stats_iterable = container.stats(decode=True) - self._streamer = StatsStreamer(stats_iterable, initial_stream_value={}) + self._streamer = ThreadedIterableStreamer(stats_iterable, initial_stream_value={}) def _log_debug(self, msg, exception=None): logger.debug(f"containers (Docker) ID: {self._container.id} - {msg} ({exception}) ") @@ -53,7 +54,7 @@ class DockerStatsFetcher: self._streamer.stop() @property - def activity_stats(self): + def activity_stats(self) -> Dict[str, Dict[str, Any]]: """Activity Stats Each successive access of activity_stats will cause computation of activity_stats @@ -63,7 +64,7 @@ class DockerStatsFetcher: self._last_stats_computed_time = time.time() return computed_activity_stats - def _compute_activity_stats(self): + def _compute_activity_stats(self) -> Dict[str, Dict[str, Any]]: with self._streamer.result_lock: io_stats = self._get_io_stats() cpu_stats = self._get_cpu_stats() @@ -78,11 +79,11 @@ class DockerStatsFetcher: } @property - def time_since_update(self): + def time_since_update(self) -> float: # In case no update, default to 1 return max(1, self._streamer.last_update_time - self._last_stats_computed_time) - def _get_cpu_stats(self): + def _get_cpu_stats(self) -> Optional[Dict[str, float]]: """Return the container CPU usage. Output: a dict {'total': 1.49} @@ -116,7 +117,7 @@ class DockerStatsFetcher: # Return the stats return stats - def _get_memory_stats(self): + def _get_memory_stats(self) -> Optional[Dict[str, float]]: """Return the container MEMORY. Output: a dict {'usage': ..., 'limit': ..., 'inactive_file': ...} @@ -139,7 +140,7 @@ class DockerStatsFetcher: # Return the stats return stats - def _get_network_stats(self): + def _get_network_stats(self) -> Optional[Dict[str, float]]: """Return the container network usage using the Docker API (v1.0 or higher). Output: a dict {'time_since_update': 3000, 'rx': 10, 'tx': 65}. @@ -168,7 +169,7 @@ class DockerStatsFetcher: # Return the stats return stats - def _get_io_stats(self): + def _get_io_stats(self) -> Optional[Dict[str, float]]: """Return the container IO usage using the Docker API (v1.0 or higher). Output: a dict {'time_since_update': 3000, 'ior': 10, 'iow': 65}. @@ -221,7 +222,7 @@ class DockerContainersExtension: self.connect() - def connect(self): + def connect(self) -> None: """Connect to the Docker server.""" # Init the Docker API Client try: @@ -236,12 +237,12 @@ class DockerContainersExtension: # return self.client.version() return {} - def stop(self): + def stop(self) -> None: # Stop all streaming threads for t in itervalues(self.stats_fetchers): t.stop() - def update(self, all_tag): + def update(self, all_tag) -> Tuple[Dict, List[Dict]]: """Update Docker stats using the input method.""" if not self.client: @@ -280,22 +281,30 @@ class DockerContainersExtension: return version_stats, container_stats @property - def key(self): + def key(self) -> str: """Return the key of the list.""" return 'name' - def generate_stats(self, container): + def generate_stats(self, container) -> Dict[str, Any]: # Init the stats for the current container stats = { 'key': self.key, - # Export name 'name': nativestr(container.name), - # Container Id 'id': container.id, - # Container Status (from attrs) 'status': container.attrs['State']['Status'], 'created': container.attrs['Created'], 'command': [], + 'io': {}, + 'cpu': {}, + 'memory': {}, + 'network': {}, + 'io_rx': None, + 'io_wx': None, + 'cpu_percent': None, + 'memory_percent': None, + 'network_rx': None, + 'network_tx': None, + 'uptime': None, } # Container Image @@ -312,37 +321,31 @@ class DockerContainersExtension: if not stats['command']: stats['command'] = None - if stats['status'] in self.CONTAINER_ACTIVE_STATUS: - started_at = container.attrs['State']['StartedAt'] - stats_fetcher = self.stats_fetchers[container.id] - activity_stats = stats_fetcher.activity_stats - stats.update(activity_stats) + if stats['status'] not in self.CONTAINER_ACTIVE_STATUS: + return stats - # Additional fields - stats['cpu_percent'] = stats["cpu"]['total'] - stats['memory_usage'] = stats["memory"].get('usage') - if stats['memory'].get('cache') is not None: - stats['memory_usage'] -= stats['memory']['cache'] - if 'time_since_update' in stats['io']: - stats['io_rx'] = stats['io'].get('ior') // stats['io'].get('time_since_update') - stats['io_wx'] = stats['io'].get('iow') // stats['io'].get('time_since_update') - if 'time_since_update' in stats['network']: - stats['network_rx'] = stats['network'].get('rx') // stats['network'].get('time_since_update') - stats['network_tx'] = stats['network'].get('tx') // stats['network'].get('time_since_update') - stats['uptime'] = pretty_date(parser.parse(started_at).astimezone(tz.tzlocal()).replace(tzinfo=None)) - # Manage special chars in command (see isse#2733) - stats['command'] = replace_special_chars(' '.join(stats['command'])) - else: - stats['io'] = {} - stats['cpu'] = {} - stats['memory'] = {} - stats['network'] = {} - stats['io_rx'] = None - stats['io_wx'] = None - stats['cpu_percent'] = None - stats['memory_percent'] = None - stats['network_rx'] = None - stats['network_tx'] = None - stats['uptime'] = None + stats_fetcher = self.stats_fetchers[container.id] + activity_stats = stats_fetcher.activity_stats + stats.update(activity_stats) + + # Additional fields + stats['cpu_percent'] = stats['cpu']['total'] + stats['memory_usage'] = stats['memory'].get('usage') + if stats['memory'].get('cache') is not None: + stats['memory_usage'] -= stats['memory']['cache'] + + if all(k in stats['io'] for k in ('ior', 'iow', 'time_since_update')): + stats['io_rx'] = stats['io']['ior'] // stats['io']['time_since_update'] + stats['io_wx'] = stats['io']['iow'] // stats['io']['time_since_update'] + + if all(k in stats['network'] for k in ('rx', 'tx', 'time_since_update')): + stats['network_rx'] = stats['network']['rx'] // stats['network']['time_since_update'] + stats['network_tx'] = stats['network']['tx'] // stats['network']['time_since_update'] + + started_at = container.attrs['State']['StartedAt'] + stats['uptime'] = pretty_date(parser.parse(started_at).astimezone(tz.tzlocal()).replace(tzinfo=None)) + + # Manage special chars in command (see issue#2733) + stats['command'] = replace_special_chars(' '.join(stats['command'])) return stats diff --git a/glances/plugins/containers/engines/podman.py b/glances/plugins/containers/engines/podman.py index 7324ccd6..c913a58d 100644 --- a/glances/plugins/containers/engines/podman.py +++ b/glances/plugins/containers/engines/podman.py @@ -7,11 +7,13 @@ """Podman Extension unit for Glances' Containers plugin.""" +import time from datetime import datetime +from typing import Any, Dict, Optional, Tuple from glances.globals import iterkeys, itervalues, nativestr, pretty_date, replace_special_chars, string_value_to_float from glances.logger import logger -from glances.plugins.containers.stats_streamer import StatsStreamer +from glances.plugins.containers.stats_streamer import ThreadedIterableStreamer # Podman library (optional and Linux-only) # https://pypi.org/project/podman/ @@ -26,63 +28,94 @@ else: class PodmanContainerStatsFetcher: - MANDATORY_FIELDS = ["CPU", "MemUsage", "MemLimit", "NetInput", "NetOutput", "BlockInput", "BlockOutput"] + MANDATORY_FIELDS = ["CPU", "MemUsage", "MemLimit", "BlockInput", "BlockOutput"] def __init__(self, container): self._container = container + # Previous stats are stored in the self._old_computed_stats variable + # We store time data to enable rate calculations to avoid complexity for consumers of the APIs exposed. + self._old_computed_stats = {} + + # Last time when output stats (results) were computed + self._last_stats_computed_time = 0 + # Threaded Streamer stats_iterable = container.stats(decode=True) - self._streamer = StatsStreamer(stats_iterable, initial_stream_value={}) - - def _log_debug(self, msg, exception=None): - logger.debug(f"containers (Podman) ID: {self._container.id} - {msg} ({exception})") - logger.debug(self._streamer.stats) + self._streamer = ThreadedIterableStreamer(stats_iterable, initial_stream_value={}) def stop(self): self._streamer.stop() - @property - def stats(self): + def get_streamed_stats(self) -> Dict[str, Any]: stats = self._streamer.stats if stats["Error"]: - self._log_debug("Stats fetching failed", stats["Error"]) + logger.error(f"containers (Podman) Container({self._container.id}): Stats fetching failed") + logger.debug(f"containers (Podman) Container({self._container.id}): ", stats) return stats["Stats"][0] @property - def activity_stats(self): - result_stats = {"cpu": {}, "memory": {}, "io": {}, "network": {}} - api_stats = self.stats + def activity_stats(self) -> Dict[str, Any]: + """Activity Stats - if any(field not in api_stats for field in self.MANDATORY_FIELDS): - self._log_debug("Missing mandatory fields") - return result_stats + Each successive access of activity_stats will cause computation of activity_stats + """ + computed_activity_stats = self._compute_activity_stats() + self._old_computed_stats = computed_activity_stats + self._last_stats_computed_time = time.time() + return computed_activity_stats + + def _compute_activity_stats(self) -> Dict[str, Dict[str, Any]]: + stats = {"cpu": {}, "memory": {}, "io": {}, "network": {}} + api_stats = self.get_streamed_stats() + + if any(field not in api_stats for field in self.MANDATORY_FIELDS) or ( + "Network" not in api_stats and any(k not in api_stats for k in ['NetInput', 'NetOutput']) + ): + logger.error(f"containers (Podman) Container({self._container.id}): Missing mandatory fields") + return stats try: - cpu_usage = float(api_stats.get("CPU", 0)) + stats["cpu"]["total"] = api_stats['CPU'] - mem_usage = float(api_stats["MemUsage"]) - mem_limit = float(api_stats["MemLimit"]) + stats["memory"]["usage"] = api_stats["MemUsage"] + stats["memory"]["limit"] = api_stats["MemLimit"] - rx = float(api_stats["NetInput"]) - tx = float(api_stats["NetOutput"]) + stats["io"]["ior"] = api_stats["BlockInput"] + stats["io"]["iow"] = api_stats["BlockOutput"] + stats["io"]["time_since_update"] = 1 + # Hardcode `time_since_update` to 1 as podman already sends at the same fixed rate per second - ior = float(api_stats["BlockInput"]) - iow = float(api_stats["BlockOutput"]) + if "Network" not in api_stats: + # For podman rooted mode + stats["network"]['rx'] = api_stats["NetInput"] + stats["network"]['tx'] = api_stats["NetOutput"] + stats["network"]['time_since_update'] = 1 + # Hardcode to 1 as podman already sends at the same fixed rate per second + elif api_stats["Network"] is not None: + # api_stats["Network"] can be None if the infra container of the pod is killed + # For podman in rootless mode + stats['network'] = { + "cumulative_rx": sum(interface["RxBytes"] for interface in api_stats["Network"].values()), + "cumulative_tx": sum(interface["TxBytes"] for interface in api_stats["Network"].values()), + } + # Using previous stats to calculate rates + old_network_stats = self._old_computed_stats.get("network") + if old_network_stats: + stats['network']['time_since_update'] = round(self.time_since_update) + stats['network']['rx'] = stats['network']['cumulative_rx'] - old_network_stats["cumulative_rx"] + stats['network']['tx'] = stats['network']['cumulative_tx'] - old_network_stats['cumulative_tx'] - # Hardcode `time_since_update` to 1 as podman - # already sends the calculated rate per second - result_stats = { - "cpu": {"total": cpu_usage}, - "memory": {"usage": mem_usage, "limit": mem_limit}, - "io": {"ior": ior, "iow": iow, "time_since_update": 1}, - "network": {"rx": rx, "tx": tx, "time_since_update": 1}, - } except ValueError as e: - self._log_debug("Non float stats values found", e) + logger.error(f"containers (Podman) Container({self._container.id}): Non float stats values found", e) - return result_stats + return stats + + @property + def time_since_update(self) -> float: + # In case no update (at startup), default to 1 + return max(1, self._streamer.last_update_time - self._last_stats_computed_time) class PodmanPodStatsFetcher: @@ -92,7 +125,7 @@ class PodmanPodStatsFetcher: # Threaded Streamer # Temporary patch to get podman extension working stats_iterable = (pod_manager.stats(decode=True) for _ in iter(int, 1)) - self._streamer = StatsStreamer(stats_iterable, initial_stream_value={}, sleep_duration=2) + self._streamer = ThreadedIterableStreamer(stats_iterable, initial_stream_value={}, sleep_duration=2) def _log_debug(self, msg, exception=None): logger.debug(f"containers (Podman): Pod Manager - {msg} ({exception})") @@ -118,13 +151,13 @@ class PodmanPodStatsFetcher: "io": io_stats or {}, "memory": memory_stats or {}, "network": network_stats or {}, - "cpu": cpu_stats or {"total": 0.0}, + "cpu": cpu_stats or {}, } result_stats[stat["CID"]] = computed_stats return result_stats - def _get_cpu_stats(self, stats): + def _get_cpu_stats(self, stats: Dict) -> Optional[Dict]: """Return the container CPU usage. Output: a dict {'total': 1.49} @@ -136,7 +169,7 @@ class PodmanPodStatsFetcher: cpu_usage = string_value_to_float(stats["CPU"].rstrip("%")) return {"total": cpu_usage} - def _get_memory_stats(self, stats): + def _get_memory_stats(self, stats) -> Optional[Dict]: """Return the container MEMORY. Output: a dict {'usage': ..., 'limit': ...} @@ -157,7 +190,7 @@ class PodmanPodStatsFetcher: return {'usage': usage, 'limit': limit, 'inactive_file': 0} - def _get_network_stats(self, stats): + def _get_network_stats(self, stats) -> Optional[Dict]: """Return the container network usage using the Docker API (v1.0 or higher). Output: a dict {'time_since_update': 3000, 'rx': 10, 'tx': 65}. @@ -180,10 +213,10 @@ class PodmanPodStatsFetcher: self._log_debug("Compute MEM usage failed", e) return None - # Hardcode `time_since_update` to 1 as podman docs don't specify the rate calculated procedure + # Hardcode `time_since_update` to 1 as podman docs don't specify the rate calculation procedure return {"rx": rx, "tx": tx, "time_since_update": 1} - def _get_io_stats(self, stats): + def _get_io_stats(self, stats) -> Optional[Dict]: """Return the container IO usage using the Docker API (v1.0 or higher). Output: a dict {'time_since_update': 3000, 'ior': 10, 'iow': 65}. @@ -206,7 +239,7 @@ class PodmanPodStatsFetcher: self._log_debug("Compute BlockIO usage failed", e) return None - # Hardcode `time_since_update` to 1 as podman docs don't specify the rate calculated procedure + # Hardcode `time_since_update` to 1 as podman docs don't specify the rate calculation procedure return {"ior": ior, "iow": iow, "time_since_update": 1} @@ -242,7 +275,7 @@ class PodmanContainersExtension: # return self.client.version() return {} - def stop(self): + def stop(self) -> None: # Stop all streaming threads for t in itervalues(self.container_stats_fetchers): t.stop() @@ -250,7 +283,7 @@ class PodmanContainersExtension: if self.pods_stats_fetcher: self.pods_stats_fetcher.stop() - def update(self, all_tag): + def update(self, all_tag) -> Tuple[Dict, list[Dict[str, Any]]]: """Update Podman stats using the input method.""" if not self.client: @@ -298,55 +331,58 @@ class PodmanContainersExtension: return version_stats, container_stats @property - def key(self): + def key(self) -> str: """Return the key of the list.""" return 'name' - def generate_stats(self, container): + def generate_stats(self, container) -> Dict[str, Any]: # Init the stats for the current container stats = { 'key': self.key, - # Export name 'name': nativestr(container.name), - # Container Id 'id': container.id, - # Container Image 'image': ','.join(container.image.tags if container.image.tags else []), - # Container Status (from attrs) 'status': container.attrs['State'], 'created': container.attrs['Created'], 'command': container.attrs.get('Command') or [], + 'io': {}, + 'cpu': {}, + 'memory': {}, + 'network': {}, + 'io_rx': None, + 'io_wx': None, + 'cpu_percent': None, + 'memory_percent': None, + 'network_rx': None, + 'network_tx': None, + 'uptime': None, } - if stats['status'] in self.CONTAINER_ACTIVE_STATUS: - started_at = datetime.fromtimestamp(container.attrs['StartedAt']) - stats_fetcher = self.container_stats_fetchers[container.id] - activity_stats = stats_fetcher.activity_stats - stats.update(activity_stats) + if stats['status'] not in self.CONTAINER_ACTIVE_STATUS: + return stats - # Additional fields - stats['cpu_percent'] = stats["cpu"]['total'] - stats['memory_usage'] = stats["memory"].get('usage') - if stats['memory'].get('cache') is not None: - stats['memory_usage'] -= stats['memory']['cache'] - stats['io_rx'] = stats['io'].get('ior') // stats['io'].get('time_since_update') - stats['io_wx'] = stats['io'].get('iow') // stats['io'].get('time_since_update') - stats['network_rx'] = stats['network'].get('rx') // stats['network'].get('time_since_update') - stats['network_tx'] = stats['network'].get('tx') // stats['network'].get('time_since_update') - stats['uptime'] = pretty_date(started_at) - # Manage special chars in command (see isse#2733) - stats['command'] = replace_special_chars(' '.join(stats['command'])) - else: - stats['io'] = {} - stats['cpu'] = {} - stats['memory'] = {} - stats['network'] = {} - stats['io_rx'] = None - stats['io_wx'] = None - stats['cpu_percent'] = None - stats['memory_percent'] = None - stats['network_rx'] = None - stats['network_tx'] = None - stats['uptime'] = None + stats_fetcher = self.container_stats_fetchers[container.id] + activity_stats = stats_fetcher.activity_stats + stats.update(activity_stats) + + # Additional fields + stats['cpu_percent'] = stats['cpu'].get('total') + stats['memory_usage'] = stats['memory'].get('usage') + if stats['memory'].get('cache') is not None: + stats['memory_usage'] -= stats['memory']['cache'] + + if all(k in stats['io'] for k in ('ior', 'iow', 'time_since_update')): + stats['io_rx'] = stats['io']['ior'] // stats['io']['time_since_update'] + stats['io_wx'] = stats['io']['iow'] // stats['io']['time_since_update'] + + if all(k in stats['network'] for k in ('rx', 'tx', 'time_since_update')): + stats['network_rx'] = stats['network']['rx'] // stats['network']['time_since_update'] + stats['network_tx'] = stats['network']['tx'] // stats['network']['time_since_update'] + + started_at = datetime.fromtimestamp(container.attrs['StartedAt']) + stats['uptime'] = pretty_date(started_at) + + # Manage special chars in command (see issue#2733) + stats['command'] = replace_special_chars(' '.join(stats['command'])) return stats diff --git a/glances/plugins/containers/stats_streamer.py b/glances/plugins/containers/stats_streamer.py index 999c8dcb..ec31dab2 100644 --- a/glances/plugins/containers/stats_streamer.py +++ b/glances/plugins/containers/stats_streamer.py @@ -11,11 +11,11 @@ import time from glances.logger import logger -class StatsStreamer: +class ThreadedIterableStreamer: """ Utility class to stream an iterable using a background / daemon Thread - Use `StatsStreamer.stats` to access the latest streamed results + Use `ThreadedIterableStreamer.stats` to access the latest streamed results """ def __init__(self, iterable, initial_stream_value=None, sleep_duration=0.1): diff --git a/glances/plugins/cpu/__init__.py b/glances/plugins/cpu/__init__.py index 63d051d6..ecc30a23 100644 --- a/glances/plugins/cpu/__init__.py +++ b/glances/plugins/cpu/__init__.py @@ -165,8 +165,6 @@ class PluginModel(GlancesPluginModel): stats = self.update_local() elif self.input_method == 'snmp': stats = self.update_snmp() - else: - stats = self.get_init_value() # Update the stats self.stats = stats @@ -185,7 +183,7 @@ class PluginModel(GlancesPluginModel): # Init new stats stats = self.get_init_value() - stats['total'] = cpu_percent.get() + stats['total'] = cpu_percent.get_cpu() # Standards stats # - user: time spent by normal processes executing in user mode; on Linux this also includes guest time diff --git a/glances/plugins/fs/__init__.py b/glances/plugins/fs/__init__.py index 6b084e6e..5507aef8 100644 --- a/glances/plugins/fs/__init__.py +++ b/glances/plugins/fs/__init__.py @@ -114,127 +114,143 @@ class PluginModel(GlancesPluginModel): @GlancesPluginModel._log_result_decorator def update(self): """Update the FS stats using the input method.""" - # Init new stats - stats = self.get_init_value() - + # Update the stats if self.input_method == 'local': - # Update stats using the standard system lib - - # Grab the stats using the psutil disk_partitions - # If 'all'=False return physical devices only (e.g. hard disks, cd-rom drives, USB keys) - # and ignore all others (e.g. memory partitions such as /dev/shm) - try: - fs_stat = psutil.disk_partitions(all=False) - except (UnicodeDecodeError, PermissionError): - logger.debug("Plugin - fs: PsUtil fetch failed") - return self.stats - - # Optional hack to allow logical mounts points (issue #448) - allowed_fs_types = self.get_conf_value('allow') - if allowed_fs_types: - # Avoid Psutil call unless mounts need to be allowed - try: - all_mounted_fs = psutil.disk_partitions(all=True) - except (UnicodeDecodeError, PermissionError): - logger.debug("Plugin - fs: PsUtil extended fetch failed") - else: - # Discard duplicates (#2299) and add entries matching allowed fs types - tracked_mnt_points = {f.mountpoint for f in fs_stat} - for f in all_mounted_fs: - if ( - any(f.fstype.find(fs_type) >= 0 for fs_type in allowed_fs_types) - and f.mountpoint not in tracked_mnt_points - ): - fs_stat.append(f) - - # Loop over fs - for fs in fs_stat: - # Hide the stats if the mount point is in the exclude list - if not self.is_display(fs.mountpoint): - continue - - # Grab the disk usage - try: - fs_usage = psutil.disk_usage(fs.mountpoint) - except OSError: - # Correct issue #346 - # Disk is ejected during the command - continue - fs_current = { - 'device_name': fs.device, - 'fs_type': fs.fstype, - # Manage non breaking space (see issue #1065) - 'mnt_point': u(fs.mountpoint).replace('\u00a0', ' '), - 'size': fs_usage.total, - 'used': fs_usage.used, - 'free': fs_usage.free, - 'percent': fs_usage.percent, - 'key': self.get_key(), - } - - # Hide the stats if the device name is in the exclude list - # Correct issue: glances.conf FS hide not applying #1666 - if not self.is_display(fs_current['device_name']): - continue - - # Add alias if exist (define in the configuration file) - if self.has_alias(fs_current['mnt_point']) is not None: - fs_current['alias'] = self.has_alias(fs_current['mnt_point']) - - stats.append(fs_current) - - elif self.input_method == 'snmp': - # Update stats using SNMP - - # SNMP bulk command to get all file system in one shot - try: - fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True) - except KeyError: - fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid['default'], bulk=True) - - # Loop over fs - if self.short_system_name in ('windows', 'esxi'): - # Windows or ESXi tips - for fs in fs_stat: - # Memory stats are grabbed in the same OID table (ignore it) - if fs == 'Virtual Memory' or fs == 'Physical Memory' or fs == 'Real Memory': - continue - size = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit']) - used = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit']) - percent = float(used * 100 / size) - fs_current = { - 'device_name': '', - 'mnt_point': fs.partition(' ')[0], - 'size': size, - 'used': used, - 'percent': percent, - 'key': self.get_key(), - } - # Do not take hidden file system into account - if self.is_hide(fs_current['mnt_point']): - continue - stats.append(fs_current) - else: - # Default behavior - for fs in fs_stat: - fs_current = { - 'device_name': fs_stat[fs]['device_name'], - 'mnt_point': fs, - 'size': int(fs_stat[fs]['size']) * 1024, - 'used': int(fs_stat[fs]['used']) * 1024, - 'percent': float(fs_stat[fs]['percent']), - 'key': self.get_key(), - } - # Do not take hidden file system into account - if self.is_hide(fs_current['mnt_point']) or self.is_hide(fs_current['device_name']): - continue - stats.append(fs_current) + stats = self.update_local() + else: + stats = self.get_init_value() # Update the stats self.stats = stats return self.stats + def update_local(self): + """Update the FS stats using the input method.""" + # Init new stats + stats = self.get_init_value() + + # Update stats using the standard system lib + + # Grab the stats using the psutil disk_partitions + # If 'all'=False return physical devices only (e.g. hard disks, cd-rom drives, USB keys) + # and ignore all others (e.g. memory partitions such as /dev/shm) + try: + fs_stat = psutil.disk_partitions(all=False) + except (UnicodeDecodeError, PermissionError): + logger.debug("Plugin - fs: PsUtil fetch failed") + return stats + + # Optional hack to allow logical mounts points (issue #448) + allowed_fs_types = self.get_conf_value('allow') + if allowed_fs_types: + # Avoid Psutil call unless mounts need to be allowed + try: + all_mounted_fs = psutil.disk_partitions(all=True) + except (UnicodeDecodeError, PermissionError): + logger.debug("Plugin - fs: PsUtil extended fetch failed") + else: + # Discard duplicates (#2299) and add entries matching allowed fs types + tracked_mnt_points = {f.mountpoint for f in fs_stat} + for f in all_mounted_fs: + if ( + any(f.fstype.find(fs_type) >= 0 for fs_type in allowed_fs_types) + and f.mountpoint not in tracked_mnt_points + ): + fs_stat.append(f) + + # Loop over fs + for fs in fs_stat: + # Hide the stats if the mount point is in the exclude list + # It avoids unnecessary call to PsUtil disk_usage + if not self.is_display(fs.mountpoint): + continue + + # Grab the disk usage + try: + fs_usage = psutil.disk_usage(fs.mountpoint) + except OSError: + # Correct issue #346 + # Disk is ejected during the command + continue + fs_current = { + 'device_name': fs.device, + 'fs_type': fs.fstype, + # Manage non breaking space (see issue #1065) + 'mnt_point': u(fs.mountpoint).replace('\u00a0', ' '), + 'size': fs_usage.total, + 'used': fs_usage.used, + 'free': fs_usage.free, + 'percent': fs_usage.percent, + 'key': self.get_key(), + } + + # Hide the stats if the device name is in the exclude list + # Correct issue: glances.conf FS hide not applying #1666 + if not self.is_display(fs_current['device_name']): + continue + + # Add alias if exist (define in the configuration file) + if self.has_alias(fs_current['mnt_point']) is not None: + fs_current['alias'] = self.has_alias(fs_current['mnt_point']) + + stats.append(fs_current) + + return stats + + def update_snmp(self): + """Update the FS stats using the input method.""" + # Init new stats + stats = self.get_init_value() + + # Update stats using SNMP + + # SNMP bulk command to get all file system in one shot + try: + fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid[self.short_system_name], bulk=True) + except KeyError: + fs_stat = self.get_stats_snmp(snmp_oid=snmp_oid['default'], bulk=True) + + # Loop over fs + if self.short_system_name in ('windows', 'esxi'): + # Windows or ESXi tips + for fs in fs_stat: + # Memory stats are grabbed in the same OID table (ignore it) + if fs == 'Virtual Memory' or fs == 'Physical Memory' or fs == 'Real Memory': + continue + size = int(fs_stat[fs]['size']) * int(fs_stat[fs]['alloc_unit']) + used = int(fs_stat[fs]['used']) * int(fs_stat[fs]['alloc_unit']) + percent = float(used * 100 / size) + fs_current = { + 'device_name': '', + 'mnt_point': fs.partition(' ')[0], + 'size': size, + 'used': used, + 'percent': percent, + 'key': self.get_key(), + } + # Do not take hidden file system into account + if self.is_hide(fs_current['mnt_point']): + continue + stats.append(fs_current) + else: + # Default behavior + for fs in fs_stat: + fs_current = { + 'device_name': fs_stat[fs]['device_name'], + 'mnt_point': fs, + 'size': int(fs_stat[fs]['size']) * 1024, + 'used': int(fs_stat[fs]['used']) * 1024, + 'percent': float(fs_stat[fs]['percent']), + 'key': self.get_key(), + } + # Do not take hidden file system into account + if self.is_hide(fs_current['mnt_point']) or self.is_hide(fs_current['device_name']): + continue + stats.append(fs_current) + + return stats + def update_views(self): """Update stats views.""" # Call the father's method diff --git a/glances/plugins/percpu/__init__.py b/glances/plugins/percpu/__init__.py index 66e2c438..bf52446d 100644 --- a/glances/plugins/percpu/__init__.py +++ b/glances/plugins/percpu/__init__.py @@ -120,16 +120,12 @@ class PluginModel(GlancesPluginModel): @GlancesPluginModel._log_result_decorator def update(self): """Update per-CPU stats using the input method.""" - # Init new stats - stats = self.get_init_value() - - # Grab per-CPU stats using psutil's cpu_percent(percpu=True) and - # cpu_times_percent(percpu=True) methods + # Grab per-CPU stats using psutil's if self.input_method == 'local': - stats = cpu_percent.get(percpu=True) + stats = cpu_percent.get_percpu() else: # Update stats using SNMP - pass + stats = self.get_init_value() # Update the stats self.stats = stats diff --git a/glances/plugins/processlist/__init__.py b/glances/plugins/processlist/__init__.py index e5be4d82..17a4af65 100644 --- a/glances/plugins/processlist/__init__.py +++ b/glances/plugins/processlist/__init__.py @@ -324,31 +324,34 @@ class PluginModel(GlancesPluginModel): def _get_process_curses_time(self, p, selected, args): """Return process time curses""" + cpu_times = p['cpu_times'] try: # Sum user and system time - user_system_time = p['cpu_times']['user'] + p['cpu_times']['system'] - except (OverflowError, TypeError): + user_system_time = cpu_times['user'] + cpu_times['system'] + except (OverflowError, TypeError, KeyError): # Catch OverflowError on some Amazon EC2 server # See https://github.com/nicolargo/glances/issues/87 # Also catch TypeError on macOS # See: https://github.com/nicolargo/glances/issues/622 + # Also catch KeyError (as no stats be present for processes of other users) + # See: https://github.com/nicolargo/glances/issues/2831 # logger.debug("Cannot get TIME+ ({})".format(e)) msg = self.layout_header['time'].format('?') - ret = self.curse_add_line(msg, optional=True) + return self.curse_add_line(msg, optional=True) + + hours, minutes, seconds = seconds_to_hms(user_system_time) + if hours > 99: + msg = f'{hours:<7}h' + elif 0 < hours < 100: + msg = f'{hours}h{minutes}:{seconds}' else: - hours, minutes, seconds = seconds_to_hms(user_system_time) - if hours > 99: - msg = f'{hours:<7}h' - elif 0 < hours < 100: - msg = f'{hours}h{minutes}:{seconds}' - else: - msg = f'{minutes}:{seconds}' - msg = self.layout_stat['time'].format(msg) - if hours > 0: - ret = self.curse_add_line(msg, decoration='CPU_TIME', optional=True) - else: - ret = self.curse_add_line(msg, optional=True) - return ret + msg = f'{minutes}:{seconds}' + + msg = self.layout_stat['time'].format(msg) + if hours > 0: + return self.curse_add_line(msg, decoration='CPU_TIME', optional=True) + + return self.curse_add_line(msg, optional=True) def _get_process_curses_thread(self, p, selected, args): """Return process thread curses""" diff --git a/glances/plugins/quicklook/__init__.py b/glances/plugins/quicklook/__init__.py index 091e4963..b36bca74 100644 --- a/glances/plugins/quicklook/__init__.py +++ b/glances/plugins/quicklook/__init__.py @@ -118,8 +118,8 @@ class PluginModel(GlancesPluginModel): # Get the CPU percent value (global and per core) # Stats is shared across all plugins - stats['cpu'] = cpu_percent.get() - stats['percpu'] = cpu_percent.get(percpu=True) + stats['cpu'] = cpu_percent.get_cpu() + stats['percpu'] = cpu_percent.get_percpu() # Get the virtual and swap memory stats['mem'] = psutil.virtual_memory().percent diff --git a/glances/plugins/wifi/__init__.py b/glances/plugins/wifi/__init__.py index dc6fc5a1..0781c978 100644 --- a/glances/plugins/wifi/__init__.py +++ b/glances/plugins/wifi/__init__.py @@ -22,13 +22,13 @@ from glances.globals import file_exists, nativestr from glances.logger import logger from glances.plugins.plugin.model import GlancesPluginModel -# Backup solution is to use the /proc/net/wireless file -# but it only give signal information about the current hotspot +# Use stats available in the /proc/net/wireless file +# Note: it only give signal information about the current hotspot WIRELESS_FILE = '/proc/net/wireless' wireless_file_exists = file_exists(WIRELESS_FILE) if not wireless_file_exists: - logger.debug(f"Wifi plugin is disabled (no {WIRELESS_FILE} file found)") + logger.debug(f"Wifi plugin is disabled (can not read {WIRELESS_FILE} file)") # Fields description # description: human readable description @@ -96,31 +96,12 @@ class PluginModel(GlancesPluginModel): return stats if self.input_method == 'local' and wireless_file_exists: - # As a backup solution, use the /proc/net/wireless file - with open(WIRELESS_FILE) as f: - # The first two lines are header - f.readline() - f.readline() - # Others lines are Wifi stats - wifi_stats = f.readline() - while wifi_stats != '': - # Extract the stats - wifi_stats = wifi_stats.split() - # Add the Wifi link to the list - stats.append( - { - 'key': self.get_key(), - 'ssid': wifi_stats[0][:-1], - 'quality_link': float(wifi_stats[2]), - 'quality_level': float(wifi_stats[3]), - } - ) - # Next line - wifi_stats = f.readline() - + try: + stats = self._get_wireless_stats() + except (PermissionError, FileNotFoundError) as e: + logger.debug(f"Wifi plugin error: can not read {WIRELESS_FILE} file ({e})") elif self.input_method == 'snmp': # Update stats using SNMP - # Not implemented yet pass @@ -129,6 +110,31 @@ class PluginModel(GlancesPluginModel): return self.stats + def _get_wireless_stats(self): + ret = self.get_init_value() + # As a backup solution, use the /proc/net/wireless file + with open(WIRELESS_FILE) as f: + # The first two lines are header + f.readline() + f.readline() + # Others lines are Wifi stats + wifi_stats = f.readline() + while wifi_stats != '': + # Extract the stats + wifi_stats = wifi_stats.split() + # Add the Wifi link to the list + ret.append( + { + 'key': self.get_key(), + 'ssid': wifi_stats[0][:-1], + 'quality_link': float(wifi_stats[2]), + 'quality_level': float(wifi_stats[3]), + } + ) + # Next line + wifi_stats = f.readline() + return ret + def get_alert(self, value): """Overwrite the default get_alert method. diff --git a/glances/processes.py b/glances/processes.py index 0b507596..9487b9ef 100644 --- a/glances/processes.py +++ b/glances/processes.py @@ -119,6 +119,14 @@ class GlancesProcesses: """Set args.""" self.args = args + def reset_internal_cache(self): + """Reset the internal cache.""" + self.cache_timer = Timer(0) + self.processlist_cache = {} + if hasattr(psutil.process_iter, 'cache_clear'): + # Cache clear only available in PsUtil 6 or higher + psutil.process_iter.cache_clear() + def reset_processcount(self): """Reset the global process count""" self.processcount = {'total': 0, 'running': 0, 'sleeping': 0, 'thread': 0, 'pid_max': None} @@ -445,7 +453,9 @@ class GlancesProcesses: ) ) # Only get the info key - processlist = [p.info for p in processlist] + # PsUtil 6+ no longer check PID reused #2755 so use is_running in the loop + # Note: not sure it is realy needed but CPU consumption look teh same with or without it + processlist = [p.info for p in processlist if p.is_running()] # Sort the processes list by the current sort_key processlist = sort_stats(processlist, sorted_by=self.sort_key, reverse=True) diff --git a/glances/stats.py b/glances/stats.py index d56983a9..e033723f 100644 --- a/glances/stats.py +++ b/glances/stats.py @@ -249,8 +249,8 @@ class GlancesStats: def load_limits(self, config=None): """Load the stats limits (except the one in the exclude list).""" - # For each plugins, call the load_limits method - for p in self._plugins: + # For each plugins (enable or not), call the load_limits method + for p in self.getPluginsList(enable=False): self._plugins[p].load_limits(config) def __update_plugin(self, p): @@ -260,19 +260,13 @@ class GlancesStats: self._plugins[p].update_views() def update(self): - """Wrapper method to update the stats. + """Wrapper method to update all stats. Only called by standalone and server modes """ - threads = [] # Start update of all enable plugins for p in self.getPluginsList(enable=True): - thread = threading.Thread(target=self.__update_plugin, args=(p,)) - thread.start() - threads.append(thread) - # Wait the end of the update - for t in threads: - t.join() + self.__update_plugin(p) def export(self, input_stats=None): """Export all the stats. @@ -286,7 +280,7 @@ class GlancesStats: input_stats = input_stats or {} - for e in self.getExportsList(enable=True): + for e in self.getExportsList(): logger.debug(f"Export stats using the {e} module") thread = threading.Thread(target=self._exports[e].update, args=(input_stats,)) thread.start() @@ -294,12 +288,20 @@ class GlancesStats: return True def getAll(self): - """Return all the stats (list).""" - return [self._plugins[p].get_raw() for p in self._plugins] + """Return all the stats (list). + This method is called byt the XML/RPC API. + It should return all the plugins (enable or not) because filtering can be done by the client. + """ + return [self._plugins[p].get_raw() for p in self.getPluginsList(enable=False)] - def getAllAsDict(self): - """Return all the stats (dict).""" - return {p: self._plugins[p].get_raw() for p in self._plugins} + def getAllAsDict(self, plugin_list=None): + """Return all the stats (as dict). + This method is called by the RESTFul API. + """ + if plugin_list is None: + # All enabled plugins should be exported + plugin_list = self.getPluginsList() + return {p: self._plugins[p].get_raw() for p in plugin_list} def getAllExports(self, plugin_list=None): """Return all the stats to be exported as a list. @@ -310,7 +312,7 @@ class GlancesStats: if plugin_list is None: # All enabled plugins should be exported plugin_list = self.getPluginsList() - return [self._plugins[p].get_export() for p in self._plugins] + return [self._plugins[p].get_export() for p in plugin_list] def getAllExportsAsDict(self, plugin_list=None): """Return all the stats to be exported as a dict. @@ -345,17 +347,23 @@ class GlancesStats: plugin_list = self.getPluginsList() return {p: self._plugins[p].limits for p in plugin_list} - def getAllViews(self): - """Return the plugins views.""" - return [self._plugins[p].get_views() for p in self._plugins] + def getAllViews(self, plugin_list=None): + """Return the plugins views. + This method is called byt the XML/RPC API. + It should return all the plugins views (enable or not) because filtering can be done by the client. + """ + if plugin_list is None: + plugin_list = self.getPluginsList(enable=False) + return [self._plugins[p].get_views() for p in plugin_list] - def getAllViewsAsDict(self): - """Return all the stats views (dict).""" - return {p: self._plugins[p].get_views() for p in self._plugins} - - def get_plugin_list(self): - """Return the plugin list.""" - return self._plugins + def getAllViewsAsDict(self, plugin_list=None): + """Return all the stats views (dict). + This method is called by the RESTFul API. + """ + if plugin_list is None: + # All enabled plugins should be exported + plugin_list = self.getPluginsList() + return {p: self._plugins[p].get_views() for p in plugin_list} def get_plugin(self, plugin_name): """Return the plugin stats.""" diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 0a0f9db3..51114d09 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,5 +1,5 @@ name: glances -version: '4.0.8' +version: '4.1.0+build01' summary: Glances an Eye on your system. A top/htop alternative. description: | @@ -31,6 +31,7 @@ apps: - removable-media - power-control - process-control + - network-setup-observe environment: LANG: C.UTF-8 LC_ALL: C.UTF-8 diff --git a/test-data/issues/issue2849.py b/test-data/issues/issue2849.py new file mode 100644 index 00000000..34691a97 --- /dev/null +++ b/test-data/issues/issue2849.py @@ -0,0 +1,25 @@ +import sys +import time + +sys.path.insert(0, '../glances') + +########### + +# from glances.cpu_percent import cpu_percent + +# for _ in range(0, 5): +# print([i['total'] for i in cpu_percent.get_percpu()]) +# time.sleep(2) + +########### + +from glances.main import GlancesMain +from glances.stats import GlancesStats + +core = GlancesMain() +stats = GlancesStats(config=core.get_config(), args=core.get_args()) + +for _ in range(0, 5): + stats.update() + print([i['total'] for i in stats.get_plugin('percpu').get_raw()]) + time.sleep(2) diff --git a/unittest-core.py b/unittest-core.py index bad2dcf9..c061d6e3 100755 --- a/unittest-core.py +++ b/unittest-core.py @@ -88,7 +88,7 @@ class TestGlances(unittest.TestCase): # Check stats self.assertIsInstance(plugin_instance.get_raw(), (dict, list)) - if isinstance(plugin_instance.get_raw(), dict): + if isinstance(plugin_instance.get_raw(), dict) and plugin_instance.get_raw() != {}: res = False for f in plugin_instance.fields_description: if f not in plugin_instance.get_raw(): @@ -96,7 +96,7 @@ class TestGlances(unittest.TestCase): else: res = True self.assertTrue(res) - elif isinstance(plugin_instance.get_raw(), list): + elif isinstance(plugin_instance.get_raw(), list) and len(plugin_instance.get_raw()) > 0: res = False for i in plugin_instance.get_raw(): for f in i: @@ -119,7 +119,7 @@ class TestGlances(unittest.TestCase): current_stats['foo'] = 'bar' current_stats = plugin_instance.filter_stats(current_stats) self.assertTrue('foo' not in current_stats) - elif isinstance(plugin_instance.get_raw(), list): + elif isinstance(plugin_instance.get_raw(), list) and len(plugin_instance.get_raw()) > 0: current_stats[0]['foo'] = 'bar' current_stats = plugin_instance.filter_stats(current_stats) self.assertTrue('foo' not in current_stats[0]) @@ -133,34 +133,36 @@ class TestGlances(unittest.TestCase): if plugin_instance.history_enable(): if isinstance(plugin_instance.get_raw(), dict): first_history_field = plugin_instance.get_items_history_list()[0]['name'] - elif isinstance(plugin_instance.get_raw(), list): + elif isinstance(plugin_instance.get_raw(), list) and len(plugin_instance.get_raw()) > 0: first_history_field = '_'.join( [ plugin_instance.get_raw()[0][plugin_instance.get_key()], plugin_instance.get_items_history_list()[0]['name'], ] ) - self.assertEqual(len(plugin_instance.get_raw_history(first_history_field)), 2) - self.assertGreater( - plugin_instance.get_raw_history(first_history_field)[1][0], - plugin_instance.get_raw_history(first_history_field)[0][0], - ) + if len(plugin_instance.get_raw()) > 0: + self.assertEqual(len(plugin_instance.get_raw_history(first_history_field)), 2) + self.assertGreater( + plugin_instance.get_raw_history(first_history_field)[1][0], + plugin_instance.get_raw_history(first_history_field)[0][0], + ) # Update stats (add third element) plugin_instance.update() plugin_instance.update_stats_history() plugin_instance.update_views() - self.assertEqual(len(plugin_instance.get_raw_history(first_history_field)), 3) - self.assertEqual(len(plugin_instance.get_raw_history(first_history_field, 2)), 2) - self.assertIsInstance(json.loads(plugin_instance.get_stats_history()), dict) + if len(plugin_instance.get_raw()) > 0: + self.assertEqual(len(plugin_instance.get_raw_history(first_history_field)), 3) + self.assertEqual(len(plugin_instance.get_raw_history(first_history_field, 2)), 2) + self.assertIsInstance(json.loads(plugin_instance.get_stats_history()), dict) # Check views self.assertIsInstance(plugin_instance.get_views(), dict) if isinstance(plugin_instance.get_raw(), dict): self.assertIsInstance(plugin_instance.get_views(first_history_field), dict) self.assertTrue('decoration' in plugin_instance.get_views(first_history_field)) - elif isinstance(plugin_instance.get_raw(), list): + elif isinstance(plugin_instance.get_raw(), list) and len(plugin_instance.get_raw()) > 0: first_history_field = plugin_instance.get_items_history_list()[0]['name'] first_item = plugin_instance.get_raw()[0][plugin_instance.get_key()] self.assertIsInstance(plugin_instance.get_views(item=first_item, key=first_history_field), dict) @@ -592,6 +594,7 @@ class TestGlances(unittest.TestCase): bar.percent = 110 self.assertEqual(bar.get(), '|||||||||||||||||||||||||||||||||||||||||||| >100%') + # Error in Github Action. Do not remove the comment. # def test_100_system_plugin_method(self): # """Test system plugin methods""" # print('INFO: [TEST_100] Test system plugin methods') @@ -623,6 +626,7 @@ class TestGlances(unittest.TestCase): print('INFO: [TEST_105] Test network plugin methods') self._common_plugin_tests('network') + # Error in Github Action. Do not remove the comment. # def test_106_diskio_plugin_method(self): # """Test diskio plugin methods""" # print('INFO: [TEST_106] Test diskio plugin methods')