From cb22e53aa65c9b6fb31d642f99bcc85b4d673d6a Mon Sep 17 00:00:00 2001 From: glances-rs Date: Fri, 9 Oct 2026 23:47:21 +0000 Subject: [PATCH] M3: extended plugins - folders, amps, ports, raid, wifi, ip (drop-in compatible stats) - differential tests pass for all 22 implemented plugins - TUI left sidebar extended --- Cargo.lock | 612 +++++++++++++++++- crates/glances-plugins/Cargo.toml | 1 + crates/glances-plugins/src/collectors.rs | 123 ++++ crates/glances-plugins/src/engine.rs | 6 + crates/glances-plugins/src/plugins/amps.rs | 243 +++++++ crates/glances-plugins/src/plugins/folders.rs | 232 +++++++ crates/glances-plugins/src/plugins/ip.rs | 181 ++++++ crates/glances-plugins/src/plugins/mod.rs | 16 +- crates/glances-plugins/src/plugins/ports.rs | 306 +++++++++ crates/glances-plugins/src/plugins/raid.rs | 186 ++++++ crates/glances-plugins/src/plugins/wifi.rs | 154 +++++ crates/glances-tui/src/lib.rs | 14 +- 12 files changed, 2059 insertions(+), 15 deletions(-) create mode 100644 crates/glances-plugins/src/plugins/amps.rs create mode 100644 crates/glances-plugins/src/plugins/folders.rs create mode 100644 crates/glances-plugins/src/plugins/ip.rs create mode 100644 crates/glances-plugins/src/plugins/ports.rs create mode 100644 crates/glances-plugins/src/plugins/raid.rs create mode 100644 crates/glances-plugins/src/plugins/wifi.rs diff --git a/Cargo.lock b/Cargo.lock index 97deebc..4a42063 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.5" @@ -62,7 +68,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -73,7 +79,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -112,6 +118,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bit-set" version = "0.5.3" @@ -175,6 +187,12 @@ version = "1.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + [[package]] name = "castaway" version = "0.2.4" @@ -296,6 +314,35 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -311,6 +358,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + [[package]] name = "critical-section" version = "1.2.0" @@ -372,7 +428,7 @@ checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" dependencies = [ "dispatch2", "nix 0.31.3", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -465,6 +521,17 @@ dependencies = [ "objc2", ] +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "document-features" version = "0.2.12" @@ -493,7 +560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -544,6 +611,17 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", + "zlib-rs", +] + [[package]] name = "fnv" version = "1.0.7" @@ -556,6 +634,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures-core" version = "0.3.34" @@ -590,6 +677,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -663,6 +761,7 @@ dependencies = [ "sysinfo", "thiserror 2.0.21", "tracing", + "ureq", ] [[package]] @@ -714,6 +813,22 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -738,12 +853,116 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.14.2" @@ -860,6 +1079,12 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + [[package]] name = "litrs" version = "1.0.0" @@ -897,7 +1122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b367b50a9be9a5d3b5718c1da74e5d6b9c17647f89752ee2562a470cc3c4eb1a" dependencies = [ "nix 0.30.1", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -936,6 +1161,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.4" @@ -945,7 +1180,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1010,7 +1245,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1182,6 +1417,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pest" version = "2.9.3" @@ -1290,6 +1531,15 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.1" @@ -1489,6 +1739,20 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -1508,7 +1772,42 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -1636,6 +1935,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + [[package]] name = "siphasher" version = "1.0.4" @@ -1654,6 +1959,12 @@ version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e" +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "stacker" version = "0.1.25" @@ -1664,7 +1975,7 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1700,6 +2011,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "1.0.109" @@ -1733,6 +2050,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "901704edd0dfe137f1987838ee4f259e4e063c31371bdb423f7ae38ec6f77f02" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + [[package]] name = "sysinfo" version = "0.39.6" @@ -1758,7 +2086,7 @@ dependencies = [ "parking_lot", "rustix", "signal-hook", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1768,7 +2096,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -1799,7 +2127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" dependencies = [ "anyhow", - "base64", + "base64 0.22.1", "bitflags 2.13.2", "fancy-regex", "filedescriptor", @@ -1896,6 +2224,7 @@ dependencies = [ "powerfmt", "serde_core", "time-core", + "time-macros", ] [[package]] @@ -1904,6 +2233,26 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tracing" version = "0.1.44" @@ -2006,6 +2355,68 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc" +dependencies = [ + "base64 0.23.1", + "cookie_store", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "ureq-proto", + "utf8-zero", + "webpki-roots", +] + +[[package]] +name = "ureq-proto" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be" +dependencies = [ + "base64 0.23.1", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2105,6 +2516,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "wezterm-bidi" version = "0.2.3" @@ -2300,6 +2720,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -2309,6 +2738,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + [[package]] name = "windows-threading" version = "0.2.1" @@ -2318,12 +2763,155 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8ebde2db3681e8c9980cc27822030e68752690ddfa9473e739aeb4dbde6d71" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75b4683f6c7f45248d4d64056a24298c6281e0993356d7d1b4a1a962ef10d4a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.6", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + [[package]] name = "zmij" version = "1.0.23" diff --git a/crates/glances-plugins/Cargo.toml b/crates/glances-plugins/Cargo.toml index d086a50..476c285 100644 --- a/crates/glances-plugins/Cargo.toml +++ b/crates/glances-plugins/Cargo.toml @@ -15,3 +15,4 @@ serde_json.workspace = true sysinfo = "0.39.6" thiserror.workspace = true tracing.workspace = true +ureq = { version = "3.4.2", features = ["json"] } diff --git a/crates/glances-plugins/src/collectors.rs b/crates/glances-plugins/src/collectors.rs index c4589fb..0719995 100644 --- a/crates/glances-plugins/src/collectors.rs +++ b/crates/glances-plugins/src/collectors.rs @@ -559,6 +559,129 @@ pub fn is_admin() -> bool { unsafe { libc::getuid() == 0 } } +/// Return the current IP address and netmask. +pub fn get_ip_address() -> (Option, Option) { + // Ask the kernel which source address it would use for default-route + // traffic (no packet is sent). + let mut ip_address: Option = None; + unsafe { + let sock = libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0); + if sock >= 0 { + let mut addr: libc::sockaddr_in = std::mem::zeroed(); + addr.sin_family = libc::AF_INET as u16; + addr.sin_port = 53u16.to_be(); + addr.sin_addr.s_addr = u32::from_be_bytes([192, 0, 2, 1]).to_be(); + if libc::connect(sock, &addr as *const _ as *const libc::sockaddr, std::mem::size_of::() as u32) == 0 { + let mut local: libc::sockaddr_in = std::mem::zeroed(); + let mut len = std::mem::size_of::() as u32; + if libc::getsockname(sock, &mut local as *mut _ as *mut libc::sockaddr, &mut len) == 0 { + let bytes = local.sin_addr.s_addr.to_ne_bytes(); + let candidate = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]); + if !candidate.starts_with("127.") && candidate != "0.0.0.0" { + ip_address = Some(candidate); + } + } + } + libc::close(sock); + } + } + + if let Some(ip) = &ip_address { + // Find the matching netmask with getifaddrs. + let mut netmask = None; + unsafe { + let mut ifap: *mut libc::ifaddrs = std::ptr::null_mut(); + if libc::getifaddrs(&mut ifap) == 0 { + let mut current = ifap; + while !current.is_null() { + let addr = (*current).ifa_addr; + let mask = (*current).ifa_netmask; + if !addr.is_null() && (*addr).sa_family as i32 == libc::AF_INET { + let sin = addr as *const libc::sockaddr_in; + let bytes = (*sin).sin_addr.s_addr.to_ne_bytes(); + let candidate = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]); + if &candidate == ip && !mask.is_null() { + let sin_mask = mask as *const libc::sockaddr_in; + let mbytes = (*sin_mask).sin_addr.s_addr.to_ne_bytes(); + netmask = Some(format!("{}.{}.{}.{}", mbytes[0], mbytes[1], mbytes[2], mbytes[3])); + break; + } + } + current = (*current).ifa_next; + } + libc::freeifaddrs(ifap); + } + } + return (ip_address, netmask); + } + + // Fallback: first up non-loopback interface with an address. + let mut found = (None, None); + unsafe { + let mut ifap: *mut libc::ifaddrs = std::ptr::null_mut(); + if libc::getifaddrs(&mut ifap) == 0 { + let mut current = ifap; + while !current.is_null() { + let addr = (*current).ifa_addr; + if !addr.is_null() && (*addr).sa_family as i32 == libc::AF_INET { + let name = std::ffi::CStr::from_ptr((*current).ifa_name).to_string_lossy().to_string(); + let flags = (*current).ifa_flags as u32; + if name != "lo" && flags & libc::IFF_UP as u32 != 0 { + let sin = addr as *const libc::sockaddr_in; + let bytes = (*sin).sin_addr.s_addr.to_ne_bytes(); + let candidate = format!("{}.{}.{}.{}", bytes[0], bytes[1], bytes[2], bytes[3]); + let mask = (*current).ifa_netmask; + let mut netmask = None; + if !mask.is_null() { + let sin_mask = mask as *const libc::sockaddr_in; + let mbytes = (*sin_mask).sin_addr.s_addr.to_ne_bytes(); + netmask = Some(format!("{}.{}.{}.{}", mbytes[0], mbytes[1], mbytes[2], mbytes[3])); + } + found = (Some(candidate), netmask); + break; + } + } + current = (*current).ifa_next; + } + libc::freeifaddrs(ifap); + } + } + found +} + +/// Return the default gateway IP address (from `/proc/net/route`). +pub fn get_default_gateway() -> Option { + let content = std::fs::read_to_string("/proc/net/route").ok()?; + for line in content.lines().skip(1) { + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() < 3 { + continue; + } + if fields[1] == "00000000" { + let gateway = fields[2]; + if gateway.len() == 8 { + let bytes: Vec = (0..4) + .map(|i| u8::from_str_radix(&gateway[i * 2..i * 2 + 2], 16).unwrap_or(0).to_string()) + .collect(); + return Some(bytes.join(".")); + } + } + } + None +} + +/// Convert a netmask to CIDR notation (port of `ip_to_cidr`). +pub fn ip_to_cidr(ip: Option<&str>) -> u32 { + match ip { + None => 0, + Some(ip) => ip + .split('.') + .filter_map(|part| part.parse::().ok()) + .map(|part| part.count_ones()) + .sum(), + } +} + /// Read a sysfs hwmon input value. pub fn read_number_file(path: &str) -> Option { std::fs::read_to_string(path).ok().and_then(|v| v.trim().parse::().ok()) diff --git a/crates/glances-plugins/src/engine.rs b/crates/glances-plugins/src/engine.rs index dddfecc..720dc7d 100644 --- a/crates/glances-plugins/src/engine.rs +++ b/crates/glances-plugins/src/engine.rs @@ -27,10 +27,13 @@ impl Engine { // Registration order mirrors the alphabetical plugin loading of the // Python implementation (in particular processcount runs before // processlist and programlist). + plugins.insert("amps".to_string(), Box::new(AmpsPlugin::new(&config, &args))); plugins.insert("core".to_string(), Box::new(CorePlugin::new(&config, &args))); plugins.insert("cpu".to_string(), Box::new(CpuPlugin::new(&config, &args))); plugins.insert("diskio".to_string(), Box::new(DiskioPlugin::new(&config, &args))); + plugins.insert("folders".to_string(), Box::new(FoldersPlugin::new(&config, &args))); plugins.insert("fs".to_string(), Box::new(FsPlugin::new(&config, &args))); + plugins.insert("ip".to_string(), Box::new(IpPlugin::new(&config, &args))); plugins.insert("irq".to_string(), Box::new(IrqPlugin::new(&config, &args))); plugins.insert("load".to_string(), Box::new(LoadPlugin::new(&config, &args))); plugins.insert("mem".to_string(), Box::new(MemPlugin::new(&config, &args))); @@ -38,15 +41,18 @@ impl Engine { plugins.insert("network".to_string(), Box::new(NetworkPlugin::new(&config, &args))); plugins.insert("now".to_string(), Box::new(NowPlugin::new(&config, &args))); plugins.insert("percpu".to_string(), Box::new(PerCpuPlugin::new(&config, &args))); + plugins.insert("ports".to_string(), Box::new(PortsPlugin::new(&config, &args))); plugins.insert("processcount".to_string(), Box::new(ProcessCountPlugin::new(&config, &args))); plugins.insert("processlist".to_string(), Box::new(ProcessListPlugin::new(&config, &args))); plugins.insert("programlist".to_string(), Box::new(ProgramListPlugin::new(&config, &args))); plugins.insert("psutilversion".to_string(), Box::new(PsutilVersionPlugin::new(&config, &args))); plugins.insert("quicklook".to_string(), Box::new(QuicklookPlugin::new(&config, &args))); + plugins.insert("raid".to_string(), Box::new(RaidPlugin::new(&config, &args))); plugins.insert("sensors".to_string(), Box::new(SensorsPlugin::new(&config, &args))); plugins.insert("system".to_string(), Box::new(SystemPlugin::new(&config, &args))); plugins.insert("uptime".to_string(), Box::new(UptimePlugin::new(&config, &args))); plugins.insert("version".to_string(), Box::new(VersionPlugin::new(&config, &args))); + plugins.insert("wifi".to_string(), Box::new(WifiPlugin::new(&config, &args))); // The refresh time is set on the model, which was built with the // command line value already. let _ = refresh_time; diff --git a/crates/glances-plugins/src/plugins/amps.rs b/crates/glances-plugins/src/plugins/amps.rs new file mode 100644 index 0000000..1a3113b --- /dev/null +++ b/crates/glances-plugins/src/plugins/amps.rs @@ -0,0 +1,243 @@ +//! AMPs plugin: run user-defined commands and report their result. + +use crate::fields; +use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext}; +use glances_core::processes::processes; +use glances_core::secure::secure_popen; +use glances_core::{Args, Config}; +use serde_json::{Value, json}; +use std::time::Instant; + +pub struct AmpsPlugin { + pub model: PluginModel, + amps: Vec, +} + +#[derive(Debug)] +struct AmpConfig { + name: String, + command: Option, + regex: Option, + refresh: f64, + countmin: Option, + countmax: Option, + one_line: bool, + enabled: bool, + timer: Instant, + result: Option, + count: i64, +} + +impl AmpsPlugin { + pub fn new(config: &Config, args: &Args) -> Self { + let fields = fields! { + "name" => Field::new("AMP name."), + "result" => Field::new("AMP result (a string)."), + "refresh" => Field::new("AMP refresh interval.").unit("second"), + "timer" => Field::new("Time until next refresh.").unit("second"), + "count" => Field::new("Number of matching processes.").unit("number"), + "countmin" => Field::new("Minimum number of matching processes.").unit("number"), + "countmax" => Field::new("Maximum number of matching processes.").unit("number"), + }; + let mut model = PluginModel::new( + "amps", + fields, + Vec::new(), + Value::Array(Vec::new()), + config, + args.time.unwrap_or(2.0), + args.disable_history, + args.disable_config_exec, + ); + model.set_item_key("name"); + + let mut amps = Vec::new(); + for section in config.sections() { + if !section.starts_with("amp_") { + continue; + } + let command = config.get_value(section, "command", None); + let enabled = config.get_bool_value(section, "enable", false); + let regex = config + .get_value(section, "regex", None) + .and_then(|pattern| regex::Regex::new(&pattern).ok()); + let refresh = config.get_float_value(section, "refresh", 3.0); + let countmin = config.get_value(section, "countmin", None).and_then(|v| v.parse::().ok()); + let countmax = config.get_value(section, "countmax", None).and_then(|v| v.parse::().ok()); + let one_line = config.get_bool_value(section, "one_line", false); + amps.push(AmpConfig { + name: amp_display_name(section), + enabled, + command, + regex, + refresh, + countmin, + countmax, + one_line, + timer: Instant::now(), + result: None, + count: 0, + }); + } + Self { model, amps } + } +} + +/// Return the AMP display name (mirrors the Python AMP module names). +fn amp_display_name(section: &str) -> String { + let raw = section.trim_start_matches("amp_"); + match raw { + "systemv" => "SystemV".to_string(), + "systemd" => "Systemd".to_string(), + "nginx" => "Nginx".to_string(), + other => { + let mut chars = other.chars(); + match chars.next() { + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + None => String::new(), + } + } + } +} + +impl Plugin for AmpsPlugin { + fn name(&self) -> &str { + "amps" + } + + fn model(&self) -> &PluginModel { + &self.model + } + + fn model_mut(&mut self) -> &mut PluginModel { + &mut self.model + } + + fn get_key(&self) -> Option<&'static str> { + Some("name") + } + + fn collect(&mut self, ctx: &mut UpdateContext) -> anyhow::Result { + let mut stats = Vec::new(); + for amp in self.amps.iter_mut() { + if amp.timer.elapsed().as_secs_f64() >= amp.refresh && amp.enabled { + let result = match &.command { + Some(command) => secure_popen(command, !ctx.args.disable_config_exec, None), + None => String::new(), + }; + amp.result = Some(if amp.one_line { + result.lines().next().unwrap_or("").to_string() + } else { + result.trim_end().to_string() + }); + amp.timer = Instant::now(); + } + if let Some(regex) = &.regex { + let procs = processes().lock().unwrap(); + amp.count = procs + .get_list(false) + .iter() + .filter(|process| { + let name = process.get("name").and_then(Value::as_str).unwrap_or(""); + let cmdline = process + .get("cmdline") + .and_then(Value::as_array) + .map(|a| a.iter().filter_map(|v| v.as_str()).collect::>().join(" ")) + .unwrap_or_default(); + regex.is_match(name) || regex.is_match(&cmdline) + }) + .count() as i64; + } + let timer_left = (amp.refresh - amp.timer.elapsed().as_secs_f64()).max(0.0); + stats.push(json!({ + "key": "name", + "name": amp.name, + "result": amp.result, + "refresh": amp.refresh, + "timer": timer_left, + "count": amp.count, + "countmin": amp.countmin, + "countmax": amp.countmax, + "regex": amp.regex.is_some(), + })); + } + Ok(Value::Array(stats)) + } + + fn update_views(&mut self) { + let key = self.get_key(); + self.model.update_views(key); + let items: Vec<(String, i64, Option, Option, bool)> = match &self.model.stats { + Value::Array(list) => list + .iter() + .filter_map(|item| { + let name = item.get("name")?.as_str()?.to_string(); + let count = item.get("count")?.as_i64()?; + let countmin = item.get("countmin").and_then(Value::as_i64); + let countmax = item.get("countmax").and_then(Value::as_i64); + let regex = item.get("regex").and_then(Value::as_bool).unwrap_or(false); + Some((name, count, countmin, countmax, regex)) + }) + .collect(), + _ => Vec::new(), + }; + for (name, count, countmin, countmax, regex) in items { + let decoration = if !regex { + glances_core::Decoration::Default + } else if countmax.is_some_and(|max| count > max) { + glances_core::Decoration::Critical + } else if countmin.is_some_and(|min| count < min) { + glances_core::Decoration::Warning + } else { + glances_core::Decoration::Ok + }; + self.model.set_view_decoration(Some(&name), "count", decoration); + } + } + + fn display_curse(&self) -> bool { + true + } + + fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec { + let mut ret = Vec::new(); + if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 { + return ret; + } + let name_max_width = ctx.max_width.saturating_sub(12); + ret.push(self.model.curse_add_line( + format!("{:9}", "Count"), glances_core::Decoration::Default, false, false, false)); + let items = self.model.sorted_stats(); + let Some(items) = items.as_array() else { + return ret; + }; + for i in items { + let name = i.get("name").and_then(Value::as_str).unwrap_or(""); + let count = i.get("count").and_then(Value::as_i64).unwrap_or(0); + let result = i.get("result").and_then(Value::as_str).unwrap_or(""); + let display_name = if result.is_empty() { name.to_string() } else { format!("{name}: {result}") }; + ret.push(self.model.curse_new_line()); + ret.push(self.model.curse_add_line( + format!("{:(), width = name_max_width), + glances_core::Decoration::Default, + false, + false, + false, + )); + ret.push(self.model.curse_add_line( + format!("{count:>9}"), + self.model.view_decoration(Some(name), "count"), + false, + false, + false, + )); + } + ret + } +} diff --git a/crates/glances-plugins/src/plugins/folders.rs b/crates/glances-plugins/src/plugins/folders.rs new file mode 100644 index 0000000..0f8f696 --- /dev/null +++ b/crates/glances-plugins/src/plugins/folders.rs @@ -0,0 +1,232 @@ +//! Folders plugin: monitor the size of configured directories. + +use crate::fields; +use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext}; +use glances_core::{Args, Config}; +use serde_json::{Value, json}; +use std::path::Path; +use std::time::Instant; + +pub struct FoldersPlugin { + pub model: PluginModel, + folders: Vec, +} + +#[derive(Debug)] +struct FolderConfig { + indice: String, + path: String, + refresh: f64, + careful: Option, + warning: Option, + critical: Option, + timer: Instant, + size: u64, + errno: i64, +} + +impl FoldersPlugin { + pub fn new(config: &Config, args: &Args) -> Self { + let fields = fields! { + "path" => Field::new("Absolute path."), + "size" => Field::new("Folder size in bytes.").unit("byte"), + "refresh" => Field::new("Refresh interval in seconds.").unit("second"), + "errno" => Field::new("Return code when retrieving folder size (0 is no error).").unit("number"), + "careful" => Field::new("Careful threshold in MB.").unit("megabyte"), + "warning" => Field::new("Warning threshold in MB.").unit("megabyte"), + "critical" => Field::new("Critical threshold in MB.").unit("megabyte"), + }; + let mut model = PluginModel::new( + "folders", + fields, + Vec::new(), + Value::Array(Vec::new()), + config, + args.time.unwrap_or(2.0), + args.disable_history, + args.disable_config_exec, + ); + model.set_item_key("path"); + + let mut folders = Vec::new(); + if config.has_section("folders") { + let default_refresh = config.get_float_value("folders", "refresh", 60.0); + for index in 1..=10 { + let key = format!("folder_{index}_"); + let Some(path) = config.get_value("folders", &format!("{key}path"), None) else { + continue; + }; + if path.is_empty() { + continue; + } + let refresh = config.get_float_value("folders", &format!("{key}refresh"), default_refresh); + let threshold = |name: &str| -> Option { + config.get_value("folders", &format!("{key}{name}"), None).and_then(|v| v.parse().ok()) + }; + folders.push(FolderConfig { + indice: index.to_string(), + path, + refresh, + careful: threshold("careful"), + warning: threshold("warning"), + critical: threshold("critical"), + timer: Instant::now() - std::time::Duration::from_secs(3600), + size: 0, + errno: 0, + }); + } + } + Self { model, folders } + } +} + +impl Plugin for FoldersPlugin { + fn name(&self) -> &str { + "folders" + } + + fn model(&self) -> &PluginModel { + &self.model + } + + fn model_mut(&mut self) -> &mut PluginModel { + &mut self.model + } + + fn get_key(&self) -> Option<&'static str> { + Some("path") + } + + fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result { + let mut stats = Vec::new(); + for folder in self.folders.iter_mut() { + if folder.timer.elapsed().as_secs_f64() >= folder.refresh { + let (size, errno) = folder_size(Path::new(&folder.path), 0); + folder.size = size; + folder.errno = errno; + folder.timer = Instant::now(); + } + let mut item = serde_json::Map::new(); + item.insert("path".to_string(), json!(folder.path)); + item.insert("size".to_string(), json!(folder.size)); + item.insert("refresh".to_string(), json!(folder.refresh as i64)); + item.insert("errno".to_string(), json!(folder.errno)); + item.insert("careful".to_string(), json!(folder.careful)); + item.insert("warning".to_string(), json!(folder.warning)); + item.insert("critical".to_string(), json!(folder.critical)); + item.insert("key".to_string(), json!("path")); + stats.push(Value::Object(item)); + } + Ok(Value::Array(stats)) + } + + fn update_views(&mut self) { + let key = self.get_key(); + self.model.update_views(key); + let items: Vec<(String, i64, u64, Option, Option, Option)> = match &self.model.stats { + Value::Array(list) => list + .iter() + .filter_map(|item| { + let path = item.get("path")?.as_str()?.to_string(); + let errno = item.get("errno")?.as_i64()?; + let size = item.get("size")?.as_u64()?; + let careful = item.get("careful").and_then(Value::as_f64); + let warning = item.get("warning").and_then(Value::as_f64); + let critical = item.get("critical").and_then(Value::as_f64); + Some((path, errno, size, careful, warning, critical)) + }) + .collect(), + _ => Vec::new(), + }; + for (path, errno, size, careful, warning, critical) in items { + if errno != 0 { + continue; + } + let decoration = if critical.is_some_and(|c| size as f64 > c * 1_000_000.0) { + glances_core::Decoration::Critical + } else if warning.is_some_and(|c| size as f64 > c * 1_000_000.0) { + glances_core::Decoration::Warning + } else if careful.is_some_and(|c| size as f64 > c * 1_000_000.0) { + glances_core::Decoration::Careful + } else { + glances_core::Decoration::Default + }; + self.model.set_view_decoration(Some(&path), "size", decoration); + } + } + + fn display_curse(&self) -> bool { + true + } + + fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec { + let mut ret = Vec::new(); + if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 { + return ret; + } + let name_max_width = ctx.max_width.saturating_sub(12); + ret.push(self.model.curse_add_line( + format!("{:9}", "Size"), glances_core::Decoration::Default, false, false, false)); + let items = self.model.sorted_stats(); + let Some(items) = items.as_array() else { + return ret; + }; + for i in items { + let path = i.get("path").and_then(Value::as_str).unwrap_or(""); + let size = i.get("size").and_then(Value::as_f64).unwrap_or(0.0); + let errno = i.get("errno").and_then(Value::as_i64).unwrap_or(0); + ret.push(self.model.curse_new_line()); + ret.push(self.model.curse_add_line( + format!("{:(), width = name_max_width), + glances_core::Decoration::Default, + false, + false, + false, + )); + let msg = if errno != 0 { + format!("?{:>8}", self.model.auto_unit(size, false, "K")) + } else { + format!("{:>9}", self.model.auto_unit(size, false, "K")) + }; + ret.push(self.model.curse_add_line( + msg, + self.model.view_decoration(Some(path), "size"), + false, + false, + false, + )); + } + ret + } +} + +/// Recursive folder size (port of `folder_size`). +fn folder_size(path: &Path, mut errno: i64) -> (u64, i64) { + let mut size = 0u64; + let entries = match std::fs::read_dir(path) { + Ok(entries) => entries, + Err(e) => return (0, e.raw_os_error().unwrap_or(0) as i64), + }; + for entry in entries.flatten() { + let entry_path = entry.path(); + match entry.file_type() { + Ok(file_type) if file_type.is_dir() => { + let (sub_size, sub_errno) = folder_size(&entry_path, errno); + size += sub_size; + errno = sub_errno; + } + Ok(_) => match entry.metadata() { + Ok(metadata) => size += metadata.len(), + Err(e) => errno = e.raw_os_error().unwrap_or(0) as i64, + }, + Err(e) => errno = e.raw_os_error().unwrap_or(0) as i64, + } + } + (size, errno) +} diff --git a/crates/glances-plugins/src/plugins/ip.rs b/crates/glances-plugins/src/plugins/ip.rs new file mode 100644 index 0000000..b5d0755 --- /dev/null +++ b/crates/glances-plugins/src/plugins/ip.rs @@ -0,0 +1,181 @@ +//! IP plugin: private and (optionally) public IP information. + +use crate::fields; +use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext}; +use glances_core::{Args, Config}; +use serde_json::{Value, json}; +use std::time::Instant; + +pub struct IpPlugin { + pub model: PluginModel, + public_api: Option, + public_field: String, + public_template: String, + public_disabled: bool, + public_username: Option, + public_password: Option, + public_interval: f64, + public_timer: Instant, + public_address: String, + public_info: Option, +} + +impl IpPlugin { + pub fn new(config: &Config, args: &Args) -> Self { + let fields = fields! { + "address" => Field::new("Private IP address"), + "mask" => Field::new("Private IP mask"), + "mask_cidr" => Field::new("Private IP mask in CIDR format").unit("number"), + "gateway" => Field::new("Private IP gateway"), + "public_address" => Field::new("Public IP address"), + "public_info_human" => Field::new("Public IP information"), + }; + let model = PluginModel::new( + "ip", + fields, + Vec::new(), + json!({}), + config, + args.time.unwrap_or(2.0), + args.disable_history, + args.disable_config_exec, + ); + let public_api = model.get_conf_value("public_api", "").first().cloned(); + let public_disabled = model + .get_conf_value("public_disabled", "") + .first() + .map(|v| v.to_lowercase() != "false") + .unwrap_or(true) + || public_api.is_none(); + Self { + model, + public_api, + public_field: "ip".to_string(), + public_template: String::new(), + public_disabled, + public_username: None, + public_password: None, + public_interval: 300.0, + public_timer: Instant::now() - std::time::Duration::from_secs(3600), + public_address: String::new(), + public_info: None, + } + } + + fn refresh_public(&mut self) { + if self.public_disabled { + return; + } + if self.public_timer.elapsed().as_secs_f64() < self.public_interval { + return; + } + self.public_timer = Instant::now(); + let Some(api) = &self.public_api else { + return; + }; + let mut request = ureq::get(api) + .config() + .timeout_global(Some(std::time::Duration::from_secs(2))) + .build(); + if let (Some(username), Some(password)) = (&self.public_username, &self.public_password) { + request = request.header("Authorization", &format!("Basic {}:{}", username, password)); + } + if let Ok(mut response) = request.call() { + if let Ok(value) = response.body_mut().read_json::() { + self.public_address = + value.get(&self.public_field).and_then(Value::as_str).unwrap_or("").to_string(); + self.public_info = Some(value); + } + } + } + + fn public_info_human(&self) -> String { + let Some(info) = &self.public_info else { + return String::new(); + }; + if self.public_template.is_empty() { + return String::new(); + } + let mut result = self.public_template.clone(); + for (key, value) in info.as_object().into_iter().flatten() { + let text = match value { + Value::String(s) => s.clone(), + other => glances_core::py_str(other), + }; + result = result.replace(&format!("{{{key}}}"), &text); + } + result + } +} + +impl Plugin for IpPlugin { + fn name(&self) -> &str { + "ip" + } + + fn model(&self) -> &PluginModel { + &self.model + } + + fn model_mut(&mut self) -> &mut PluginModel { + &mut self.model + } + + fn collect(&mut self, ctx: &mut UpdateContext) -> anyhow::Result { + let mut stats = serde_json::Map::new(); + let (address, mask) = crate::collectors::get_ip_address(); + if let Some(address) = &address { + stats.insert("address".to_string(), json!(address)); + } + if let Some(mask) = &mask { + stats.insert("mask".to_string(), json!(mask)); + } + stats.insert("mask_cidr".to_string(), json!(crate::collectors::ip_to_cidr(mask.as_deref()))); + self.refresh_public(); + if !self.public_address.is_empty() { + let public = if ctx.args.hide_public_info { + mask_public_ip(&self.public_address) + } else { + self.public_address.clone() + }; + stats.insert("public_address".to_string(), json!(public)); + let human = self.public_info_human(); + if !human.is_empty() { + stats.insert("public_info_human".to_string(), json!(human)); + } + } + Ok(Value::Object(stats)) + } + + fn display_curse(&self) -> bool { + true + } + + fn render(&mut self, _ctx: &glances_core::RenderContext) -> Vec { + let mut ret = Vec::new(); + let s = &self.model.stats; + let Some(address) = s.get("address").and_then(Value::as_str) else { + return ret; + }; + let mut text = address.to_string(); + if let Some(mask_cidr) = s.get("mask_cidr").and_then(Value::as_u64) { + if mask_cidr > 0 { + text += &format!("/{mask_cidr}"); + } + } + ret.push(self.model.curse_add_line(text, glances_core::Decoration::Default, false, false, false)); + if let Some(gateway) = s.get("gateway").and_then(Value::as_str) { + ret.push(self.model.curse_add_line(format!(" gw {gateway}"), glances_core::Decoration::Default, true, false, false)); + } + ret + } +} + +fn mask_public_ip(address: &str) -> String { + let parts: Vec<&str> = address.split('.').collect(); + if parts.len() == 4 { + format!("{}.{}.{}.xxx", parts[0], parts[1], parts[2]) + } else { + "xxx".to_string() + } +} diff --git a/crates/glances-plugins/src/plugins/mod.rs b/crates/glances-plugins/src/plugins/mod.rs index d9ce231..d6a4010 100644 --- a/crates/glances-plugins/src/plugins/mod.rs +++ b/crates/glances-plugins/src/plugins/mod.rs @@ -1,12 +1,15 @@ //! Plugin implementations. //! -//! The extended plugins (containers, GPU, ports, ...) are added in a later -//! milestone. +//! Remaining Python plugins (containers, GPU/NPU, connections, vms, cloud, +//! smart) are not ported yet; see the README status section. +pub mod amps; pub mod core; pub mod cpu; pub mod diskio; +pub mod folders; pub mod fs; +pub mod ip; pub mod irq; pub mod load; pub mod mem; @@ -14,20 +17,26 @@ pub mod memswap; pub mod network; pub mod now; pub mod percpu; +pub mod ports; pub mod processcount; pub mod processlist; pub mod programlist; pub mod psutilversion; pub mod quicklook; +pub mod raid; pub mod sensors; pub mod system; pub mod uptime; pub mod version; +pub mod wifi; +pub use amps::AmpsPlugin; pub use core::CorePlugin; pub use cpu::CpuPlugin; pub use diskio::DiskioPlugin; +pub use folders::FoldersPlugin; pub use fs::FsPlugin; +pub use ip::IpPlugin; pub use irq::IrqPlugin; pub use load::LoadPlugin; pub use mem::MemPlugin; @@ -35,12 +44,15 @@ pub use memswap::MemSwapPlugin; pub use network::NetworkPlugin; pub use now::NowPlugin; pub use percpu::PerCpuPlugin; +pub use ports::PortsPlugin; pub use processcount::ProcessCountPlugin; pub use processlist::ProcessListPlugin; pub use programlist::ProgramListPlugin; pub use psutilversion::PsutilVersionPlugin; pub use quicklook::QuicklookPlugin; +pub use raid::RaidPlugin; pub use sensors::SensorsPlugin; pub use system::SystemPlugin; pub use uptime::UptimePlugin; pub use version::VersionPlugin; +pub use wifi::WifiPlugin; diff --git a/crates/glances-plugins/src/plugins/ports.rs b/crates/glances-plugins/src/plugins/ports.rs new file mode 100644 index 0000000..712d5f0 --- /dev/null +++ b/crates/glances-plugins/src/plugins/ports.rs @@ -0,0 +1,306 @@ +//! Ports plugin: check the availability of configured hosts and ports. + +use crate::fields; +use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext}; +use glances_core::{Args, Config}; +use serde_json::{Value, json}; +use std::net::{TcpStream, ToSocketAddrs}; +use std::time::{Duration, Instant}; + +pub struct PortsPlugin { + pub model: PluginModel, + entries: Vec, + default_timeout: f64, +} + +#[derive(Debug)] +struct PortEntry { + indice: String, + host: String, + port: Option, + url: Option, + description: String, + refresh: f64, + timeout: f64, + rtt_warning: Option, + timer: Instant, + status: Option, +} + +impl PortsPlugin { + pub fn new(config: &Config, args: &Args) -> Self { + let fields = fields! { + "host" => Field::new("Measurement is be done on this host (or IP address)"), + "port" => Field::new("Measurement is be done on this port (0 for ICMP)"), + "url" => Field::new("Measurement is be done on this URL (credentials are redacted)"), + "description" => Field::new("Human readable description for the host/port"), + "refresh" => Field::new("Refresh time (in seconds) for this host/port"), + "timeout" => Field::new("Timeout (in seconds) for the measurement"), + "status" => Field::new("Measurement result (in seconds)").unit("second"), + "rtt_warning" => Field::new("Warning threshold (in seconds) for the measurement").unit("second"), + "indice" => Field::new("Unique indice for the host/port"), + }; + let mut model = PluginModel::new( + "ports", + fields, + Vec::new(), + Value::Array(Vec::new()), + config, + args.time.unwrap_or(2.0), + args.disable_history, + args.disable_config_exec, + ); + model.set_item_key("indice"); + + let default_timeout = config.get_float_value("ports", "timeout", 3.0); + let refresh = config.get_float_value("ports", "refresh", 30.0); + let mut entries = Vec::new(); + if config.has_section("ports") { + // Default gateway entry. + if config.get_bool_value("ports", "port_default_gateway", false) { + if let Some(gateway) = crate::collectors::get_default_gateway() { + entries.push(PortEntry { + indice: "port_0".to_string(), + host: gateway, + port: None, + url: None, + description: "Default gateway".to_string(), + refresh, + timeout: default_timeout, + rtt_warning: None, + timer: Instant::now() - Duration::from_secs(3600), + status: None, + }); + } + } + for index in 1..=255 { + let key = format!("port_{index}_"); + let Some(host) = config.get_value("ports", &format!("{key}host"), None) else { + continue; + }; + if host.is_empty() { + continue; + } + let port = config.get_value("ports", &format!("{key}port"), None).and_then(|v| v.parse().ok()); + let description = config + .get_value("ports", &format!("{key}description"), None) + .unwrap_or_else(|| match port { + Some(port) => format!("{host}:{port}"), + None => host.clone(), + }); + let timeout = config.get_float_value("ports", &format!("{key}timeout"), default_timeout); + let rtt_warning = config + .get_value("ports", &format!("{key}rtt_warning"), None) + .and_then(|v| v.parse::().ok()) + .map(|v| v / 1000.0); + entries.push(PortEntry { + indice: format!("port_{index}"), + host, + port, + url: None, + description, + refresh, + timeout, + rtt_warning, + timer: Instant::now() - Duration::from_secs(3600), + status: None, + }); + } + for index in 1..=255 { + let key = format!("web_{index}_"); + let Some(url) = config.get_value("ports", &format!("{key}url"), None) else { + continue; + }; + if url.is_empty() { + continue; + } + let description = config.get_value("ports", &format!("{key}description"), None).unwrap_or_else(|| url.clone()); + let timeout = config.get_float_value("ports", &format!("{key}timeout"), default_timeout); + let rtt_warning = config + .get_value("ports", &format!("{key}rtt_warning"), None) + .and_then(|v| v.parse::().ok()) + .map(|v| v / 1000.0); + entries.push(PortEntry { + indice: format!("web_{index}"), + host: url.clone(), + port: None, + url: Some(url), + description, + refresh, + timeout, + rtt_warning, + timer: Instant::now() - Duration::from_secs(3600), + status: None, + }); + } + } + Self { model, entries, default_timeout } + } + + fn check(entry: &PortEntry) -> Option { + let start = Instant::now(); + if let Some(url) = &entry.url { + let timeout = Duration::from_secs_f64(entry.timeout.max(0.1)); + let request = ureq::get(url).config().timeout_global(Some(timeout)).build(); + match request.call() { + Ok(_) => Some(start.elapsed().as_secs_f64()), + Err(_) => None, + } + } else if let Some(port) = entry.port { + let timeout = Duration::from_secs_f64(entry.timeout.max(0.1)); + let address = format!("{}:{}", entry.host, port); + let mut resolved = address.to_socket_addrs().ok()?; + let socket = resolved.next()?; + match TcpStream::connect_timeout(&socket, timeout) { + Ok(_) => Some(start.elapsed().as_secs_f64()), + Err(_) => None, + } + } else { + // ICMP check with the ping command. + let output = std::process::Command::new("ping") + .args(["-c", "1", "-W", &format!("{}", entry.timeout.max(1.0) as i64), &entry.host]) + .output() + .ok()?; + let text = String::from_utf8_lossy(&output.stdout); + if output.status.success() { + for part in text.split_whitespace() { + if let Some(value) = part.strip_prefix("time=") { + return value.parse::().ok().map(|ms| ms / 1000.0); + } + } + Some(start.elapsed().as_secs_f64()) + } else { + None + } + } + } +} + +impl Plugin for PortsPlugin { + fn name(&self) -> &str { + "ports" + } + + fn model(&self) -> &PluginModel { + &self.model + } + + fn model_mut(&mut self) -> &mut PluginModel { + &mut self.model + } + + fn get_key(&self) -> Option<&'static str> { + Some("indice") + } + + fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result { + let mut stats = Vec::new(); + for entry in self.entries.iter_mut() { + if entry.timer.elapsed().as_secs_f64() >= entry.refresh { + entry.status = Self::check(entry); + entry.timer = Instant::now(); + } + let mut item = serde_json::Map::new(); + item.insert("host".to_string(), json!(entry.host)); + item.insert("port".to_string(), json!(entry.port)); + if let Some(url) = &entry.url { + item.insert("url".to_string(), json!(redact_url(url))); + } + item.insert("description".to_string(), json!(entry.description)); + item.insert("refresh".to_string(), json!(entry.refresh)); + item.insert("timeout".to_string(), json!(entry.timeout)); + item.insert("status".to_string(), json!(entry.status)); + item.insert("rtt_warning".to_string(), json!(entry.rtt_warning)); + item.insert("indice".to_string(), json!(entry.indice)); + item.insert("key".to_string(), json!("indice")); + stats.push(Value::Object(item)); + } + let _ = self.default_timeout; + Ok(Value::Array(stats)) + } + + fn update_views(&mut self) { + let key = self.get_key(); + self.model.update_views(key); + let items: Vec<(String, Option, Option)> = match &self.model.stats { + Value::Array(list) => list + .iter() + .filter_map(|item| { + let indice = item.get("indice")?.as_str()?.to_string(); + let status = item.get("status").and_then(Value::as_f64); + let warning = item.get("rtt_warning").and_then(Value::as_f64); + Some((indice, status, warning)) + }) + .collect(), + _ => Vec::new(), + }; + for (indice, status, warning) in items { + let decoration = match status { + None => glances_core::Decoration::Critical, + Some(status) if warning.is_some_and(|w| status > w) => glances_core::Decoration::Warning, + Some(_) => glances_core::Decoration::Ok, + }; + self.model.set_view_decoration(Some(&indice), "status", decoration); + } + } + + fn display_curse(&self) -> bool { + true + } + + fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec { + let mut ret = Vec::new(); + if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 { + return ret; + } + let name_max_width = ctx.max_width.saturating_sub(12); + ret.push(self.model.curse_add_line( + format!("{:9}", "RTT"), glances_core::Decoration::Default, false, false, false)); + let items = self.model.sorted_stats(); + let Some(items) = items.as_array() else { + return ret; + }; + for i in items { + let indice = i.get("indice").and_then(Value::as_str).unwrap_or(""); + let description = i.get("description").and_then(Value::as_str).unwrap_or(""); + let status = i.get("status").and_then(Value::as_f64); + ret.push(self.model.curse_new_line()); + ret.push(self.model.curse_add_line( + format!("{:(), width = name_max_width), + glances_core::Decoration::Default, + false, + false, + false, + )); + let msg = match status { + Some(status) => format!("{:>8.3}s", status), + None => format!("{:>9}", "timeout"), + }; + ret.push(self.model.curse_add_line( + msg, + self.model.view_decoration(Some(indice), "status"), + false, + false, + false, + )); + } + ret + } +} + +/// Hide the credentials embedded in a URL. +fn redact_url(url: &str) -> String { + if let Some(scheme_end) = url.find("://") { + let rest = &url[scheme_end + 3..]; + if let Some(at) = rest.find('@') { + return format!("{}://********@{}", &url[..scheme_end], &rest[at + 1..]); + } + } + url.to_string() +} diff --git a/crates/glances-plugins/src/plugins/raid.rs b/crates/glances-plugins/src/plugins/raid.rs new file mode 100644 index 0000000..dbf5938 --- /dev/null +++ b/crates/glances-plugins/src/plugins/raid.rs @@ -0,0 +1,186 @@ +//! RAID plugin: parse `/proc/mdstat`. + +use crate::fields; +use glances_core::plugin::{Plugin, PluginModel, UpdateContext}; +use glances_core::{Args, Config}; +use serde_json::{Value, json}; + +pub struct RaidPlugin { + pub model: PluginModel, +} + +impl RaidPlugin { + pub fn new(config: &Config, args: &Args) -> Self { + let model = PluginModel::new( + "raid", + fields! {}, + Vec::new(), + json!({}), + config, + args.time.unwrap_or(2.0), + args.disable_history, + args.disable_config_exec, + ); + Self { model } + } +} + +impl Plugin for RaidPlugin { + fn name(&self) -> &str { + "raid" + } + + fn model(&self) -> &PluginModel { + &self.model + } + + fn model_mut(&mut self) -> &mut PluginModel { + &mut self.model + } + + fn available(&mut self) -> bool { + std::path::Path::new("/proc/mdstat").exists() + } + + fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result { + let mut stats = serde_json::Map::new(); + let Ok(content) = std::fs::read_to_string("/proc/mdstat") else { + return Ok(Value::Object(stats)); + }; + let lines: Vec<&str> = content.lines().collect(); + let mut index = 0; + while index < lines.len() { + let line = lines[index]; + if !line.starts_with("md") || !line.contains(':') { + index += 1; + continue; + } + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() < 3 { + index += 1; + continue; + } + let name = fields[0].to_string(); + let status = fields[2].to_string(); + let raid_type = fields + .iter() + .find(|f| f.starts_with("raid") || f.starts_with("linear") || f.starts_with("multipath")) + .map(|f| f.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + // Devices (name[index]). + let mut components = serde_json::Map::new(); + for field in fields.iter().skip(3) { + if let Some(device) = field.split('[').next() { + if !device.is_empty() { + components.insert(device.to_string(), json!("active sync")); + } + } + } + // The following line contains blocks and [used/available] [UU]. + let mut used = components.len(); + let mut available = components.len(); + if index + 1 < lines.len() { + let detail = lines[index + 1]; + if let Some(start) = detail.rfind('[') { + if let Some(end) = detail[start..].find(']') { + let counts = &detail[start + 1..start + end]; + if let Some((u, a)) = counts.split_once('/') { + used = u.parse().unwrap_or(used as i64) as usize; + available = a.parse().unwrap_or(available as i64) as usize; + } + } + } + if let Some(start) = detail.rfind('[') { + if let Some(end) = detail[start..].find(']') { + let flags = &detail[start + 1..start + end]; + if flags.len() == components.len() { + let names: Vec = components.keys().cloned().collect(); + for (position, flag) in flags.chars().enumerate() { + if let Some(device) = names.get(position) { + let status = match flag { + 'U' => "active sync", + '_' => "faulty", + 'S' => "spare", + _ => "unknown", + }; + components.insert(device.clone(), json!(status)); + } + } + } + } + } + } + stats.insert( + name.clone(), + json!({ + "name": name, + "status": status, + "type": raid_type, + "used": used, + "available": available, + "components": components, + }), + ); + index += 2; + } + Ok(Value::Object(stats)) + } + + fn display_curse(&self) -> bool { + true + } + + fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec { + let mut ret = Vec::new(); + let Some(arrays) = self.model.stats.as_object() else { + return ret; + }; + if arrays.is_empty() || ctx.max_width == 0 { + return ret; + } + let name_max_width = ctx.max_width.saturating_sub(12); + ret.push(self.model.curse_add_line( + format!("{:7}", "Used"), glances_core::Decoration::Default, false, false, false)); + ret.push(self.model.curse_add_line(format!("{:>7}", "Avail"), glances_core::Decoration::Default, false, false, false)); + let mut names: Vec<&String> = arrays.keys().collect(); + names.sort(); + for name in names { + let Some(array) = arrays.get(name).and_then(Value::as_object) else { + continue; + }; + let status = array.get("status").and_then(Value::as_str).unwrap_or(""); + let used = array.get("used").and_then(Value::as_i64).unwrap_or(0); + let available = array.get("available").and_then(Value::as_i64).unwrap_or(0); + let array_type = array.get("type").and_then(Value::as_str).unwrap_or("unknown"); + let decoration = raid_alert(status, used, available, array_type); + ret.push(self.model.curse_new_line()); + ret.push(self.model.curse_add_line( + format!("{:(), width = name_max_width), + decoration, + false, + false, + false, + )); + ret.push(self.model.curse_add_line(format!("{used:>7}"), glances_core::Decoration::Default, false, false, false)); + ret.push(self.model.curse_add_line(format!("{available:>7}"), glances_core::Decoration::Default, false, false, false)); + } + ret + } +} + + +fn raid_alert(status: &str, used: i64, available: i64, array_type: &str) -> glances_core::Decoration { + if status != "active" || used < available { + return glances_core::Decoration::Critical; + } + if array_type == "raid0" { + return glances_core::Decoration::Warning; + } + glances_core::Decoration::Ok +} diff --git a/crates/glances-plugins/src/plugins/wifi.rs b/crates/glances-plugins/src/plugins/wifi.rs new file mode 100644 index 0000000..c362ff2 --- /dev/null +++ b/crates/glances-plugins/src/plugins/wifi.rs @@ -0,0 +1,154 @@ +//! Wi-Fi plugin (reads `/proc/net/wireless`). + +use crate::fields; +use glances_core::plugin::{Field, Plugin, PluginModel, UpdateContext}; +use glances_core::{Args, Config, Decoration}; +use serde_json::{Value, json}; + +pub struct WifiPlugin { + pub model: PluginModel, +} + +impl WifiPlugin { + pub fn new(config: &Config, args: &Args) -> Self { + let fields = fields! { + "ssid" => Field::new("Wi-Fi network name."), + "quality_link" => Field::new("Signal quality level.").unit("dBm"), + "quality_level" => Field::new("Signal strong level.").unit("dBm").alert(), + }; + let mut model = PluginModel::new( + "wifi", + fields, + Vec::new(), + Value::Array(Vec::new()), + config, + args.time.unwrap_or(2.0), + args.disable_history, + args.disable_config_exec, + ); + model.set_item_key("ssid"); + Self { model } + } +} + +impl Plugin for WifiPlugin { + fn name(&self) -> &str { + "wifi" + } + + fn model(&self) -> &PluginModel { + &self.model + } + + fn model_mut(&mut self) -> &mut PluginModel { + &mut self.model + } + + fn get_key(&self) -> Option<&'static str> { + Some("ssid") + } + + fn available(&mut self) -> bool { + std::path::Path::new("/proc/net/wireless").exists() + } + + fn collect(&mut self, _ctx: &mut UpdateContext) -> anyhow::Result { + let mut stats = Vec::new(); + let Ok(content) = std::fs::read_to_string("/proc/net/wireless") else { + return Ok(Value::Array(stats)); + }; + for line in content.lines().skip(2) { + let fields: Vec<&str> = line.split_whitespace().collect(); + if fields.len() < 4 { + continue; + } + let ssid = fields[0].trim_end_matches(':').to_string(); + let quality_link = fields[2].trim_end_matches('.').parse::().unwrap_or(0.0); + let quality_level = fields[3].trim_end_matches('.').parse::().unwrap_or(0.0); + stats.push(json!({ + "key": "ssid", + "ssid": ssid, + "quality_link": quality_link, + "quality_level": quality_level, + })); + } + Ok(Value::Array(stats)) + } + + fn update_views(&mut self) { + let key = self.get_key(); + self.model.update_views(key); + let items: Vec<(String, f64)> = match &self.model.stats { + Value::Array(list) => list + .iter() + .filter_map(|item| { + let ssid = item.get("ssid")?.as_str()?.to_string(); + let level = item.get("quality_level")?.as_f64()?; + Some((ssid, level)) + }) + .collect(), + _ => Vec::new(), + }; + for (ssid, level) in items { + // Signal quality: lower is better, so the comparison is inverted. + let critical = self.model.get_limit("critical", "wifi"); + let warning = self.model.get_limit("warning", "wifi"); + let careful = self.model.get_limit("careful", "wifi"); + let decoration = if critical.is_some_and(|c| level < c) { + Decoration::Critical + } else if warning.is_some_and(|c| level < c) { + Decoration::Warning + } else if careful.is_some_and(|c| level < c) { + Decoration::Careful + } else if critical.is_none() && warning.is_none() && careful.is_none() { + Decoration::Default + } else { + Decoration::Ok + }; + self.model.set_view_decoration(Some(&ssid), "quality_level", decoration); + } + } + + fn display_curse(&self) -> bool { + true + } + + fn render(&mut self, ctx: &glances_core::RenderContext) -> Vec { + let mut ret = Vec::new(); + if self.model.stats.as_array().map(|a| a.is_empty()).unwrap_or(true) || ctx.max_width == 0 { + return ret; + } + let if_name_max_width = ctx.max_width.saturating_sub(5); + ret.push(self.model.curse_add_line( + format!("{:7}", "dBm"), glances_core::Decoration::Default, false, false, false)); + let Some(items) = self.model.stats.as_array() else { + return ret; + }; + for i in items { + let ssid = i.get("ssid").and_then(Value::as_str).unwrap_or(""); + let level = i.get("quality_level").and_then(Value::as_f64).unwrap_or(0.0); + ret.push(self.model.curse_new_line()); + ret.push(self.model.curse_add_line( + format!("{:(), width = if_name_max_width), + glances_core::Decoration::Default, + false, + false, + false, + )); + ret.push(self.model.curse_add_line( + format!("{:>7}", level), + self.model.view_decoration(Some(ssid), "quality_level"), + false, + false, + false, + )); + } + ret + } +} diff --git a/crates/glances-tui/src/lib.rs b/crates/glances-tui/src/lib.rs index 58bebc2..ec612e6 100644 --- a/crates/glances-tui/src/lib.rs +++ b/crates/glances-tui/src/lib.rs @@ -17,7 +17,19 @@ use ratatui::widgets::{Block, Borders, Clear, Paragraph, Wrap}; use std::io::Stdout; use std::time::{Duration, Instant}; -const LEFT_SIDEBAR: [&str; 8] = ["network", "diskio", "fs", "irq", "sensors", "now", "folders", "raid"]; +const LEFT_SIDEBAR: [&str; 11] = [ + "network", + "ports", + "wifi", + "diskio", + "fs", + "irq", + "folders", + "raid", + "sensors", + "ip", + "now", +]; const TOP_PLUGINS: [&str; 6] = ["cpu", "percpu", "mem", "memswap", "load", "gpu"]; #[derive(Debug, Clone, Copy, PartialEq, Eq)]