From 9edde9b133dd56bfe99bccaa93345626ed26d53c Mon Sep 17 00:00:00 2001 From: Clement Tsang <34804052+ClementTsang@users.noreply.github.com> Date: Mon, 10 Apr 2023 05:52:46 -0400 Subject: [PATCH] refactor: migrate disk collection code off of heim, remove heim (#1064) Migrates existing heim-based disk data collection code off of it to either sysinfo or vendored code based on heim/sysinfo/other sources. This also allows us to remove heim completely from bottom. --- * refactor: fix some refresh code * remove async from the freebsd code * some file/implementation organization Turns out sysinfo lacks a lot of data I need. I can still use it for the Windows disk usage implementation, but I'm probably going to manually implement macos/linux usage and all io usage stats. * more restructuring * Some other fixes * remove futures * ready for some big changes? * big changes * linux io + reads * use lossy conversion for mount point * add windows refresh * so long heim, and thanks for all the fish * fix filter behaviour, remove string allocation when reading lines * rename unix -> system for more accurate file struct representation * fix freebsd * port generic unix partition code * add bindings and fix errors * finish macOS bindings for I/O * disable conform check, this seems to... make disk I/O work on macOS????? * fix linux * add safety comments * more comments * update changelog * changelog * We're going full 0.9.0 for this * update lock * fix some typing * bleh * some file management * hoist out get_disk_usage * fix some stuff for Windows * typing and remove dead code allow lint * unify typing * fix * fix 2 * macOS fix * Add bindings file for windows * add windows implementation * fix macos --- CHANGELOG.md | 8 +- Cargo.lock | 460 +----------------- Cargo.toml | 12 +- src/app/data_farmer.rs | 42 +- src/app/data_harvester.rs | 79 ++- src/app/data_harvester/disks.rs | 172 ++++++- src/app/data_harvester/disks/freebsd.rs | 56 +-- src/app/data_harvester/disks/heim.rs | 139 ------ src/app/data_harvester/disks/heim/linux.rs | 34 -- .../disks/heim/windows_macos.rs | 14 - src/app/data_harvester/disks/io_counters.rs | 30 ++ src/app/data_harvester/disks/unix.rs | 73 +++ .../data_harvester/disks/unix/file_systems.rs | 141 ++++++ .../disks/unix/linux/counters.rs | 85 ++++ .../data_harvester/disks/unix/linux/mod.rs | 5 + .../disks/unix/linux/partition.rs | 197 ++++++++ .../disks/unix/macos/counters.rs | 49 ++ .../data_harvester/disks/unix/macos/io_kit.rs | 10 + .../disks/unix/macos/io_kit/bindings.rs | 59 +++ .../disks/unix/macos/io_kit/io_disks.rs | 24 + .../disks/unix/macos/io_kit/io_iterator.rs | 54 ++ .../disks/unix/macos/io_kit/io_object.rs | 140 ++++++ .../data_harvester/disks/unix/macos/mod.rs | 4 + .../disks/unix/other/bindings.rs | 46 ++ .../data_harvester/disks/unix/other/mod.rs | 4 + .../disks/unix/other/partition.rs | 99 ++++ src/app/data_harvester/disks/unix/usage.rs | 32 ++ src/app/data_harvester/disks/windows.rs | 75 +++ .../data_harvester/disks/windows/bindings.rs | 198 ++++++++ src/app/data_harvester/memory/arc.rs | 10 +- src/app/data_harvester/temperature/linux.rs | 4 +- src/app/filter.rs | 8 +- src/bin/main.rs | 3 - src/data_conversion.rs | 1 + src/lib.rs | 3 +- src/utils/error.rs | 10 - src/widgets/disk_table.rs | 6 +- 37 files changed, 1622 insertions(+), 764 deletions(-) delete mode 100644 src/app/data_harvester/disks/heim.rs delete mode 100644 src/app/data_harvester/disks/heim/linux.rs delete mode 100644 src/app/data_harvester/disks/heim/windows_macos.rs create mode 100644 src/app/data_harvester/disks/io_counters.rs create mode 100644 src/app/data_harvester/disks/unix.rs create mode 100644 src/app/data_harvester/disks/unix/file_systems.rs create mode 100644 src/app/data_harvester/disks/unix/linux/counters.rs create mode 100644 src/app/data_harvester/disks/unix/linux/mod.rs create mode 100644 src/app/data_harvester/disks/unix/linux/partition.rs create mode 100644 src/app/data_harvester/disks/unix/macos/counters.rs create mode 100644 src/app/data_harvester/disks/unix/macos/io_kit.rs create mode 100644 src/app/data_harvester/disks/unix/macos/io_kit/bindings.rs create mode 100644 src/app/data_harvester/disks/unix/macos/io_kit/io_disks.rs create mode 100644 src/app/data_harvester/disks/unix/macos/io_kit/io_iterator.rs create mode 100644 src/app/data_harvester/disks/unix/macos/io_kit/io_object.rs create mode 100644 src/app/data_harvester/disks/unix/macos/mod.rs create mode 100644 src/app/data_harvester/disks/unix/other/bindings.rs create mode 100644 src/app/data_harvester/disks/unix/other/mod.rs create mode 100644 src/app/data_harvester/disks/unix/other/partition.rs create mode 100644 src/app/data_harvester/disks/unix/usage.rs create mode 100644 src/app/data_harvester/disks/windows.rs create mode 100644 src/app/data_harvester/disks/windows/bindings.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f226b988..cead9d4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.9.0]/[0.8.1] - Unreleased +## [0.9.0] - Unreleased ## Bug Fixes - [#1021](https://github.com/ClementTsang/bottom/pull/1021): Fix selected text background colour being wrong if only the foreground colour was set. - [#1037](https://github.com/ClementTsang/bottom/pull/1037): Fix `is_list_ignored` accepting all results if set to `false`. +- [#1064](https://github.com/ClementTsang/bottom/pull/1064): Disk name/mount filter now doesn't always show all entries if one filter wasn't set. +- [#1064](https://github.com/ClementTsang/bottom/pull/1064): macOS disk I/O is potentially working now. +- [#597](https://github.com/ClementTsang/bottom/issues/597): Resolve RUSTSEC-2021-0119 by removing heim. ## Features @@ -25,6 +28,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#1036](https://github.com/ClementTsang/bottom/pull/1036): Migrate away from heim for memory information; Linux platforms will also now try to use `MemAvailable` to determine used memory if supported. - [#1041](https://github.com/ClementTsang/bottom/pull/1041): Migrate away from heim for network information. +- [#1064](https://github.com/ClementTsang/bottom/pull/1064): Migrate away from heim for storage information. +- [#812](https://github.com/ClementTsang/bottom/issues/812): Fully remove heim from bottom. +- [#1075](https://github.com/ClementTsang/bottom/issues/1075): Update how drives are named in Windows. ## Other diff --git a/Cargo.lock b/Cargo.lock index ac4dd4f3..154af504 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,110 +52,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "async-channel" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" -dependencies = [ - "concurrent-queue", - "event-listener", - "futures-core", -] - -[[package]] -name = "async-executor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "once_cell", - "slab", -] - -[[package]] -name = "async-fs" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b3ca4f8ff117c37c278a2f7415ce9be55560b846b5bc4412aaa5d29c1c3dae2" -dependencies = [ - "async-lock", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-io" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b" -dependencies = [ - "concurrent-queue", - "futures-lite", - "libc", - "log", - "once_cell", - "parking", - "polling", - "slab", - "socket2", - "waker-fn", - "winapi", -] - -[[package]] -name = "async-lock" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" -dependencies = [ - "event-listener", -] - -[[package]] -name = "async-net" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5373304df79b9b4395068fb080369ec7178608827306ce4d081cba51cac551df" -dependencies = [ - "async-io", - "blocking", - "futures-lite", -] - -[[package]] -name = "async-process" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83137067e3a2a6a06d67168e49e68a0957d215410473a740cea95a2425c0b7c6" -dependencies = [ - "async-io", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "libc", - "once_cell", - "signal-hook", - "winapi", -] - -[[package]] -name = "async-task" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" - -[[package]] -name = "atomic-waker" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" - [[package]] name = "atty" version = "0.2.14" @@ -194,23 +90,9 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" -[[package]] -name = "blocking" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" -dependencies = [ - "async-channel", - "async-task", - "atomic-waker", - "fastrand", - "futures-lite", - "once_cell", -] - [[package]] name = "bottom" -version = "0.8.1" +version = "0.9.0" dependencies = [ "anyhow", "assert_cmd", @@ -222,15 +104,13 @@ dependencies = [ "clap_mangen", "concat-string", "const_format", + "core-foundation 0.9.3", "crossterm 0.26.1", "ctrlc", "dirs", "fern", "filedescriptor", - "futures", - "futures-timer", "fxhash", - "heim", "humantime", "humantime-serde", "indexmap", @@ -276,12 +156,6 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" -[[package]] -name = "cache-padded" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" - [[package]] name = "cargo-husky" version = "1.5.0" @@ -357,15 +231,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7439becb5fafc780b6f4de382b1a7a3e70234afe783854a4702ee8adbb838609" -[[package]] -name = "concurrent-queue" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" -dependencies = [ - "cache-padded", -] - [[package]] name = "const_format" version = "0.2.30" @@ -402,7 +267,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ - "core-foundation-sys 0.8.3", + "core-foundation-sys 0.8.4", "libc", ] @@ -414,9 +279,9 @@ checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "crossbeam-channel" @@ -620,21 +485,6 @@ dependencies = [ "libc", ] -[[package]] -name = "event-listener" -version = "2.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" - -[[package]] -name = "fastrand" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" -dependencies = [ - "instant", -] - [[package]] name = "fern" version = "0.6.2" @@ -670,116 +520,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "futures" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608" - -[[package]] -name = "futures-executor" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" - -[[package]] -name = "futures-lite" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "memchr", - "parking", - "pin-project-lite", - "waker-fn", -] - -[[package]] -name = "futures-macro" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", -] - -[[package]] -name = "futures-sink" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364" - -[[package]] -name = "futures-task" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366" - -[[package]] -name = "futures-timer" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" - -[[package]] -name = "futures-util" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - [[package]] name = "fxhash" version = "0.2.1" @@ -818,65 +558,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -[[package]] -name = "heim" -version = "0.1.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a653442b9bdd11a77d3753a60443c60c4437d3acac8e6c3d4a6a9acd7cceed" -dependencies = [ - "heim-common", - "heim-disk", - "heim-runtime", -] - -[[package]] -name = "heim-common" -version = "0.1.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767e6e47cf88abe7c9a5ebb4df82f180d30d9c0ba0269b6d166482461765834" -dependencies = [ - "cfg-if", - "core-foundation 0.9.3", - "futures-core", - "futures-util", - "lazy_static", - "libc", - "mach", - "nix 0.19.1", - "pin-utils", - "uom", - "winapi", -] - -[[package]] -name = "heim-disk" -version = "0.1.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75603ff3868851c04954ee86bf610a6bd45be2732a0e81c35fd72b2b90fa4718" -dependencies = [ - "bitflags", - "cfg-if", - "core-foundation 0.9.3", - "heim-common", - "heim-runtime", - "libc", - "mach", - "widestring", - "winapi", -] - -[[package]] -name = "heim-runtime" -version = "0.1.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54ec7e5238c8f0dd0cc60914d31a5a7aadd4cde74c966a76c1caed1f5224e9b8" -dependencies = [ - "futures", - "futures-timer", - "once_cell", - "smol", -] - [[package]] name = "hermit-abi" version = "0.1.19" @@ -924,15 +605,6 @@ dependencies = [ "hashbrown", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - [[package]] name = "io-lifetimes" version = "1.0.4" @@ -1074,18 +746,6 @@ dependencies = [ "windows-sys 0.42.0", ] -[[package]] -name = "nix" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2" -dependencies = [ - "bitflags", - "cc", - "cfg-if", - "libc", -] - [[package]] name = "nix" version = "0.23.1" @@ -1126,27 +786,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "num-integer" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.14" @@ -1210,12 +849,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" -[[package]] -name = "parking" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" - [[package]] name = "parking_lot" version = "0.12.1" @@ -1239,31 +872,6 @@ dependencies = [ "windows-sys 0.42.0", ] -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "polling" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" -dependencies = [ - "cfg-if", - "libc", - "log", - "wepoll-ffi", - "winapi", -] - [[package]] name = "predicates" version = "2.1.5" @@ -1521,46 +1129,12 @@ dependencies = [ "libc", ] -[[package]] -name = "slab" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" - [[package]] name = "smallvec" version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" -[[package]] -name = "smol" -version = "1.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cf3b5351f3e783c1d79ab5fc604eeed8b8ae9abd36b166e8b87a089efd85e4" -dependencies = [ - "async-channel", - "async-executor", - "async-fs", - "async-io", - "async-lock", - "async-net", - "async-process", - "blocking", - "futures-lite", - "once_cell", -] - -[[package]] -name = "socket2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "starship-battery" version = "0.7.9" @@ -1639,7 +1213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c2f3ca6693feb29a89724516f016488e9aafc7f37264f898593ee4b942f31b" dependencies = [ "cfg-if", - "core-foundation-sys 0.8.3", + "core-foundation-sys 0.8.4", "libc", "ntapi", "once_cell", @@ -1810,7 +1384,6 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e76503e636584f1e10b9b3b9498538279561adcef5412927ba00c2b32c4ce5ed" dependencies = [ - "num-rational", "num-traits", "typenum", ] @@ -1824,12 +1397,6 @@ dependencies = [ "libc", ] -[[package]] -name = "waker-fn" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" - [[package]] name = "walkdir" version = "2.3.2" @@ -1853,21 +1420,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "wepoll-ffi" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" -dependencies = [ - "cc", -] - -[[package]] -name = "widestring" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c" - [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index ee8ac946..b188ace4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bottom" -version = "0.8.1" +version = "0.9.0" authors = ["Clement Tsang "] edition = "2021" repository = "https://github.com/ClementTsang/bottom" @@ -82,8 +82,6 @@ crossterm = "0.26.1" ctrlc = { version = "3.2.5", features = ["termination"] } dirs = "5.0.0" fern = { version = "0.6.2", optional = true } -futures = "0.3.26" -futures-timer = "3.0.2" fxhash = "0.2.1" humantime = "2.1.0" humantime-serde = "1.1.1" @@ -109,17 +107,19 @@ unicode-width = "0.1.10" libc = "0.2.141" [target.'cfg(target_os = "linux")'.dependencies] -heim = { version = "0.1.0-rc.1", features = ["disk"] } procfs = { version = "0.15.1", default-features = false } [target.'cfg(target_os = "macos")'.dependencies] -heim = { version = "0.1.0-rc.1", features = ["disk"] } +core-foundation = "0.9.3" mach2 = "0.4.1" [target.'cfg(target_os = "windows")'.dependencies] -heim = { version = "0.1.0-rc.1", features = ["disk"] } windows = { version = "0.48.0", features = [ "Win32_Foundation", + "Win32_Security", + "Win32_Storage_FileSystem", + "Win32_System_IO", + "Win32_System_Ioctl", "Win32_System_ProcessStatus", "Win32_System_Threading", ] } diff --git a/src/app/data_farmer.rs b/src/app/data_farmer.rs index d17c0545..b36946dc 100644 --- a/src/app/data_farmer.rs +++ b/src/app/data_farmer.rs @@ -16,8 +16,6 @@ use std::{collections::BTreeMap, time::Instant, vec::Vec}; use fxhash::FxHashMap; -use once_cell::sync::Lazy; -use regex::Regex; #[cfg(feature = "battery")] use crate::data_harvester::batteries; @@ -317,23 +315,43 @@ impl DataCollection { &mut self, disks: Vec, io: disks::IoHarvest, harvested_time: Instant, ) { // TODO: [PO] To implement - let time_since_last_harvest = harvested_time .duration_since(self.current_instant) .as_secs_f64(); for (itx, device) in disks.iter().enumerate() { - if let Some(trim) = device.name.split('/').last() { - let io_device = if cfg!(target_os = "macos") { - // Must trim one level further for macOS! - static DISK_REGEX: Lazy = Lazy::new(|| Regex::new(r"disk\d+").unwrap()); - if let Some(disk_trim) = DISK_REGEX.find(trim) { - io.get(disk_trim.as_str()) + let checked_name = { + cfg_if::cfg_if! { + if #[cfg(target_os = "windows")] { + match &device.volume_name { + Some(volume_name) => Some(volume_name.as_str()), + None => device.name.split('/').last(), + } } else { - None + device.name.split('/').last() + } + } + }; + + if let Some(checked_name) = checked_name { + let io_device = { + cfg_if::cfg_if! { + if #[cfg(target_os = "macos")] { + use once_cell::sync::Lazy; + use regex::Regex; + + // Must trim one level further for macOS! + static DISK_REGEX: Lazy = + Lazy::new(|| Regex::new(r"disk\d+").unwrap()); + if let Some(new_name) = DISK_REGEX.find(checked_name) { + io.get(new_name.as_str()) + } else { + None + } + } else { + io.get(checked_name) + } } - } else { - io.get(trim) }; if let Some(io_device) = io_device { diff --git a/src/app/data_harvester.rs b/src/app/data_harvester.rs index fce310e4..77e301f6 100644 --- a/src/app/data_harvester.rs +++ b/src/app/data_harvester.rs @@ -171,18 +171,27 @@ impl DataCollector { } } + // Sysinfo-related list refreshing. if self.widgets_to_harvest.use_net { self.sys.refresh_networks_list(); } + if self.widgets_to_harvest.use_temp { self.sys.refresh_components_list(); } + #[cfg(target_os = "windows")] - if self.widgets_to_harvest.use_proc { - self.sys.refresh_users_list(); + { + if self.widgets_to_harvest.use_proc { + self.sys.refresh_users_list(); + } + + if self.widgets_to_harvest.use_disk { + self.sys.refresh_disks_list(); + } } - futures::executor::block_on(self.update_data()); + self.update_data(); std::thread::sleep(std::time::Duration::from_millis(250)); self.data.cleanup(); @@ -208,7 +217,13 @@ impl DataCollector { self.show_average_cpu = show_average_cpu; } - /// Refresh sysinfo data. + /// Refresh sysinfo data. We use sysinfo for the following data: + /// - CPU usage + /// - Memory usage + /// - Network usage + /// - Processes (non-Linux) + /// - Disk (Windows) + /// - Temperatures (non-Linux) fn refresh_sysinfo_data(&mut self) { // Refresh once every minute. If it's too frequent it can cause segfaults. const LIST_REFRESH_TIME: Duration = Duration::from_secs(60); @@ -229,9 +244,14 @@ impl DataCollector { self.sys.refresh_networks(); } + // sysinfo is used on non-Linux systems for the following: + // - Processes (users list as well for Windows) + // - Disks (Windows only) + // - Temperatures and temperature components list. #[cfg(not(target_os = "linux"))] { if self.widgets_to_harvest.use_proc { + // For Windows, sysinfo also handles the users list. #[cfg(target_os = "windows")] if refresh_start.duration_since(self.last_collection_time) > LIST_REFRESH_TIME { self.sys.refresh_users_list(); @@ -247,9 +267,17 @@ impl DataCollector { self.sys.refresh_components(); } } + + #[cfg(target_os = "windows")] + if self.widgets_to_harvest.use_disk { + if refresh_start.duration_since(self.last_collection_time) > LIST_REFRESH_TIME { + self.sys.refresh_disks_list(); + } + self.sys.refresh_disks(); + } } - pub async fn update_data(&mut self) { + pub fn update_data(&mut self) { self.refresh_sysinfo_data(); let current_instant = Instant::now(); @@ -262,27 +290,11 @@ impl DataCollector { ); self.update_temps(); self.update_network_usage(current_instant); + self.update_disks(); #[cfg(feature = "battery")] self.update_batteries(); - let (disk_res, io_res) = futures::join!( - disks::get_disk_usage( - self.widgets_to_harvest.use_disk, - &self.filters.disk_filter, - &self.filters.mount_filter, - ), - disks::get_io_usage(self.widgets_to_harvest.use_disk) - ); - - if let Ok(disks) = disk_res { - self.data.disks = disks; - } - - if let Ok(io) = io_res { - self.data.io = io; - } - // Update times for future reference. self.last_collection_time = current_instant; self.data.last_collection_time = current_instant; @@ -440,6 +452,29 @@ impl DataCollector { } } } + + #[inline] + fn update_disks(&mut self) { + if self.widgets_to_harvest.use_disk { + #[cfg(any(target_os = "freebsd", target_os = "linux", target_os = "macos"))] + { + let disk_filter = &self.filters.disk_filter; + let mount_filter = &self.filters.mount_filter; + self.data.disks = disks::get_disk_usage(disk_filter, mount_filter).ok(); + } + + #[cfg(target_os = "windows")] + { + self.data.disks = Some(disks::get_disk_usage( + &self.sys, + &self.filters.disk_filter, + &self.filters.mount_filter, + )); + } + + self.data.io = disks::get_io_usage().ok(); + } + } } #[cfg(target_os = "freebsd")] diff --git a/src/app/data_harvester/disks.rs b/src/app/data_harvester/disks.rs index b1d554ef..fbd06a94 100644 --- a/src/app/data_harvester/disks.rs +++ b/src/app/data_harvester/disks.rs @@ -1,22 +1,37 @@ -//! Data collection for disks (IO, usage, space, etc.). -//! -//! For Linux, macOS, and Windows, this is handled by heim. For FreeBSD there is a custom -//! implementation. +//! Data collection about disks (e.g. I/O, usage, space). -cfg_if::cfg_if! { - if #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))] { - pub mod heim; - pub use self::heim::*; - } else if #[cfg(target_os = "freebsd")] { - pub mod freebsd; - pub use self::freebsd::*; +use std::collections::HashMap; + +use crate::app::filter::Filter; +use cfg_if::cfg_if; + +cfg_if! { + if #[cfg(target_os = "freebsd")] { + mod freebsd; + pub(crate) use self::freebsd::*; + } else if #[cfg(target_os = "windows")] { + mod windows; + pub(crate) use self::windows::*; + } else if #[cfg(target_os = "linux")] { + mod unix; + pub(crate) use self::unix::*; + } else if #[cfg(target_os = "macos")] { + mod unix; + pub(crate) use self::unix::*; } + // TODO: Add dummy impls here for other OSes? } #[derive(Debug, Clone, Default)] pub struct DiskHarvest { pub name: String, pub mount_point: String, + + /// Windows also contains an additional volume name field. + #[cfg(target_os = "windows")] + pub volume_name: Option, + + // TODO: Maybe unify all these? pub free_space: Option, pub used_space: Option, pub total_space: Option, @@ -28,4 +43,137 @@ pub struct IoData { pub write_bytes: u64, } -pub type IoHarvest = std::collections::HashMap>; +pub type IoHarvest = HashMap>; + +cfg_if! { + if #[cfg(not(target_os = "freebsd"))] { + mod io_counters; + pub use io_counters::IoCounters; + + /// Returns the I/O usage of certain mount points. + pub fn get_io_usage() -> anyhow::Result { + let mut io_hash: HashMap> = HashMap::new(); + + for io in io_stats()?.into_iter().flatten() { + let mount_point = io.device_name().to_string_lossy(); + + io_hash.insert( + mount_point.to_string(), + Some(IoData { + read_bytes: io.read_bytes(), + write_bytes: io.write_bytes(), + }), + ); + } + + Ok(io_hash) + } + } +} + +/// Whether to keep the current disk entry given the filters, disk name, and disk mount. +/// Precedence ordering in the case where name and mount filters disagree, "allow" +/// takes precedence over "deny". +/// +/// For implementation, we do this as follows: +/// +/// 1. Is the entry allowed through any filter? That is, does it match an entry in a +/// filter where `is_list_ignored` is `false`? If so, we always keep this entry. +/// 2. Is the entry denied through any filter? That is, does it match an entry in a +/// filter where `is_list_ignored` is `true`? If so, we always deny this entry. +/// 3. Anything else is allowed. +pub(self) fn keep_disk_entry( + disk_name: &str, mount_point: &str, disk_filter: &Option, mount_filter: &Option, +) -> bool { + match (disk_filter, mount_filter) { + (Some(d), Some(m)) => match (d.is_list_ignored, m.is_list_ignored) { + (true, true) => !(d.has_match(disk_name) || m.has_match(mount_point)), + (true, false) => { + if m.has_match(mount_point) { + true + } else { + d.keep_entry(disk_name) + } + } + (false, true) => { + if d.has_match(disk_name) { + true + } else { + m.keep_entry(mount_point) + } + } + (false, false) => d.has_match(disk_name) || m.has_match(mount_point), + }, + (Some(d), None) => d.keep_entry(disk_name), + (None, Some(m)) => m.keep_entry(mount_point), + (None, None) => true, + } +} + +#[cfg(test)] +mod test { + use regex::Regex; + + use crate::app::filter::Filter; + + use super::keep_disk_entry; + + fn run_filter(disk_filter: &Option, mount_filter: &Option) -> Vec { + let targets = [ + ("/dev/nvme0n1p1", "/boot"), + ("/dev/nvme0n1p2", "/"), + ("/dev/nvme0n1p3", "/home"), + ("/dev/sda1", "/mnt/test"), + ("/dev/sda2", "/mnt/boot"), + ]; + + targets + .into_iter() + .enumerate() + .filter_map(|(itx, (name, mount))| { + if keep_disk_entry(name, mount, disk_filter, mount_filter) { + Some(itx) + } else { + None + } + }) + .collect() + } + + #[test] + fn test_keeping_disk_entry() { + let disk_ignore = Some(Filter { + is_list_ignored: true, + list: vec![Regex::new("nvme").unwrap()], + }); + + let disk_keep = Some(Filter { + is_list_ignored: false, + list: vec![Regex::new("nvme").unwrap()], + }); + + let mount_ignore = Some(Filter { + is_list_ignored: true, + list: vec![Regex::new("boot").unwrap()], + }); + + let mount_keep = Some(Filter { + is_list_ignored: false, + list: vec![Regex::new("boot").unwrap()], + }); + + assert_eq!(run_filter(&None, &None), vec![0, 1, 2, 3, 4]); + + assert_eq!(run_filter(&disk_ignore, &None), vec![3, 4]); + assert_eq!(run_filter(&disk_keep, &None), vec![0, 1, 2]); + + assert_eq!(run_filter(&None, &mount_ignore), vec![1, 2, 3]); + assert_eq!(run_filter(&None, &mount_keep), vec![0, 4]); + + assert_eq!(run_filter(&disk_ignore, &mount_ignore), vec![3]); + assert_eq!(run_filter(&disk_keep, &mount_ignore), vec![0, 1, 2, 3]); + + assert_eq!(run_filter(&disk_ignore, &mount_keep), vec![0, 3, 4]); + assert_eq!(run_filter(&disk_keep, &mount_keep), vec![0, 1, 2, 4]); + } +} diff --git a/src/app/data_harvester/disks/freebsd.rs b/src/app/data_harvester/disks/freebsd.rs index a7eb7a8c..5755fc75 100644 --- a/src/app/data_harvester/disks/freebsd.rs +++ b/src/app/data_harvester/disks/freebsd.rs @@ -4,9 +4,8 @@ use std::io; use serde::Deserialize; -use super::{DiskHarvest, IoHarvest}; -use crate::app::Filter; -use crate::data_harvester::deserialize_xo; +use super::{keep_disk_entry, DiskHarvest, IoHarvest}; +use crate::{app::Filter, data_harvester::deserialize_xo, utils::error}; #[derive(Deserialize, Debug, Default)] #[serde(rename_all = "kebab-case")] @@ -24,11 +23,7 @@ struct FileSystem { mounted_on: String, } -pub async fn get_io_usage(actually_get: bool) -> crate::utils::error::Result> { - if !actually_get { - return Ok(None); - } - +pub fn get_io_usage() -> error::Result { let io_harvest = get_disk_info().map(|storage_system_information| { storage_system_information .filesystem @@ -36,36 +31,19 @@ pub async fn get_io_usage(actually_get: bool) -> crate::utils::error::Result, mount_filter: &Option, -) -> crate::utils::error::Result>> { - if !actually_get { - return Ok(None); - } - +pub fn get_disk_usage( + disk_filter: &Option, mount_filter: &Option, +) -> error::Result> { let vec_disks: Vec = get_disk_info().map(|storage_system_information| { storage_system_information .filesystem .into_iter() .filter_map(|disk| { - // Precedence ordering in the case where name and mount filters disagree, "allow" - // takes precedence over "deny". - // - // For implementation, we do this as follows: - // - // 1. Is the entry allowed through any filter? That is, does it match an entry in a - // filter where `is_list_ignored` is `false`? If so, we always keep this entry. - // 2. Is the entry denied through any filter? That is, does it match an entry in a - // filter where `is_list_ignored` is `true`? If so, we always deny this entry. - // 3. Anything else is allowed. - let filter_check_map = - [(disk_filter, &disk.name), (mount_filter, &disk.mounted_on)]; - if matches_allow_list(filter_check_map.as_slice()) - || !matches_ignore_list(filter_check_map.as_slice()) - { + if keep_disk_entry(&disk.name, &disk.mounted_on, disk_filter, mount_filter) { Some(DiskHarvest { free_space: Some(disk.available_blocks * 1024), used_space: Some(disk.used_blocks * 1024), @@ -80,21 +58,7 @@ pub async fn get_disk_usage( .collect() })?; - Ok(Some(vec_disks)) -} - -fn matches_allow_list(filter_check_map: &[(&Option, &String)]) -> bool { - filter_check_map.iter().any(|(filter, text)| match filter { - Some(f) if !f.is_list_ignored => f.list.iter().any(|r| r.is_match(text)), - Some(_) | None => false, - }) -} - -fn matches_ignore_list(filter_check_map: &[(&Option, &String)]) -> bool { - filter_check_map.iter().any(|(filter, text)| match filter { - Some(f) if f.is_list_ignored => f.list.iter().any(|r| r.is_match(text)), - Some(_) | None => false, - }) + Ok(vec_disks) } fn get_disk_info() -> io::Result { diff --git a/src/app/data_harvester/disks/heim.rs b/src/app/data_harvester/disks/heim.rs deleted file mode 100644 index ad8b08eb..00000000 --- a/src/app/data_harvester/disks/heim.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! Disk stats through heim. -//! Supports macOS, Linux, and Windows. - -use crate::app::Filter; -use crate::data_harvester::disks::{DiskHarvest, IoData, IoHarvest}; - -cfg_if::cfg_if! { - if #[cfg(target_os = "linux")] { - pub mod linux; - pub use linux::*; - } else if #[cfg(any(target_os = "macos", target_os = "windows"))] { - pub mod windows_macos; - pub use windows_macos::*; - } -} - -pub async fn get_io_usage(actually_get: bool) -> crate::utils::error::Result> { - if !actually_get { - return Ok(None); - } - - use futures::StreamExt; - - let mut io_hash: std::collections::HashMap> = - std::collections::HashMap::new(); - - let counter_stream = heim::disk::io_counters().await?; - futures::pin_mut!(counter_stream); - - while let Some(io) = counter_stream.next().await { - if let Ok(io) = io { - let mount_point = io.device_name().to_str().unwrap_or("Name Unavailable"); - - io_hash.insert( - mount_point.to_string(), - Some(IoData { - read_bytes: io.read_bytes().get::(), - write_bytes: io.write_bytes().get::(), - }), - ); - } - } - - Ok(Some(io_hash)) -} - -pub async fn get_disk_usage( - actually_get: bool, disk_filter: &Option, mount_filter: &Option, -) -> crate::utils::error::Result>> { - if !actually_get { - return Ok(None); - } - - use futures::StreamExt; - - let mut vec_disks: Vec = Vec::new(); - let partitions_stream = heim::disk::partitions_physical().await?; - futures::pin_mut!(partitions_stream); - - while let Some(part) = partitions_stream.next().await { - if let Ok(partition) = part { - let name = get_device_name(&partition); - - let mount_point = (partition - .mount_point() - .to_str() - .unwrap_or("Name Unavailable")) - .to_string(); - - // Precedence ordering in the case where name and mount filters disagree, "allow" takes precedence over "deny". - // - // For implementation, we do this as follows: - // 1. Is the entry allowed through any filter? That is, does it match an entry in a filter where `is_list_ignored` is `false`? If so, we always keep this entry. - // 2. Is the entry denied through any filter? That is, does it match an entry in a filter where `is_list_ignored` is `true`? If so, we always deny this entry. - // 3. Anything else is allowed. - - let filter_check_map = [(disk_filter, &name), (mount_filter, &mount_point)]; - - // This represents case 1. That is, if there is a match in an allowing list - if there is, then - // immediately allow it! - let matches_allow_list = filter_check_map.iter().any(|(filter, text)| { - if let Some(filter) = filter { - if !filter.is_list_ignored { - for r in &filter.list { - if r.is_match(text) { - return true; - } - } - } - } - false - }); - - let to_keep = if matches_allow_list { - true - } else { - // If it doesn't match an allow list, then check if it is denied. - // That is, if it matches in a reject filter, then reject. Otherwise, we always keep it. - !filter_check_map.iter().any(|(filter, text)| { - if let Some(filter) = filter { - if filter.is_list_ignored { - for r in &filter.list { - if r.is_match(text) { - return true; - } - } - } - } - false - }) - }; - - if to_keep { - // The usage line can fail in some cases (for example, if you use Void Linux + LUKS, - // see https://github.com/ClementTsang/bottom/issues/419 for details). As such, check - // it like this instead. - if let Ok(usage) = heim::disk::usage(partition.mount_point()).await { - vec_disks.push(DiskHarvest { - free_space: Some(usage.free().get::()), - used_space: Some(usage.used().get::()), - total_space: Some(usage.total().get::()), - mount_point, - name, - }); - } else { - vec_disks.push(DiskHarvest { - free_space: None, - used_space: None, - total_space: None, - mount_point, - name, - }); - } - } - } - } - - Ok(Some(vec_disks)) -} diff --git a/src/app/data_harvester/disks/heim/linux.rs b/src/app/data_harvester/disks/heim/linux.rs deleted file mode 100644 index cbc99d9f..00000000 --- a/src/app/data_harvester/disks/heim/linux.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Linux-specific things for Heim disk data collection. - -use heim::disk::Partition; - -pub fn get_device_name(partition: &Partition) -> String { - if let Some(device) = partition.device() { - // See if this disk is actually mounted elsewhere on Linux... - // This is a workaround to properly map I/O in some cases (i.e. disk encryption), see - // https://github.com/ClementTsang/bottom/issues/419 - if let Ok(path) = std::fs::read_link(device) { - if path.is_absolute() { - path.into_os_string() - } else { - let mut combined_path = std::path::PathBuf::new(); - combined_path.push(device); - combined_path.pop(); // Pop the current file... - combined_path.push(path); - - if let Ok(canon_path) = std::fs::canonicalize(combined_path) { - // Resolve the local path into an absolute one... - canon_path.into_os_string() - } else { - device.to_os_string() - } - } - } else { - device.to_os_string() - } - .into_string() - .unwrap_or_else(|_| "Name Unavailable".to_string()) - } else { - "Name Unavailable".to_string() - } -} diff --git a/src/app/data_harvester/disks/heim/windows_macos.rs b/src/app/data_harvester/disks/heim/windows_macos.rs deleted file mode 100644 index 428733bf..00000000 --- a/src/app/data_harvester/disks/heim/windows_macos.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! macOS and Windows-specific things for Heim disk data collection. - -use heim::disk::Partition; - -pub fn get_device_name(partition: &Partition) -> String { - if let Some(device) = partition.device() { - device - .to_os_string() - .into_string() - .unwrap_or_else(|_| "Name Unavailable".to_string()) - } else { - "Name Unavailable".to_string() - } -} diff --git a/src/app/data_harvester/disks/io_counters.rs b/src/app/data_harvester/disks/io_counters.rs new file mode 100644 index 00000000..2b07fa5d --- /dev/null +++ b/src/app/data_harvester/disks/io_counters.rs @@ -0,0 +1,30 @@ +use std::ffi::OsStr; + +#[derive(Debug, Default)] +pub struct IoCounters { + name: String, + read_bytes: u64, + write_bytes: u64, +} + +impl IoCounters { + pub fn new(name: String, read_bytes: u64, write_bytes: u64) -> Self { + Self { + name, + read_bytes, + write_bytes, + } + } + + pub(crate) fn device_name(&self) -> &OsStr { + OsStr::new(&self.name) + } + + pub(crate) fn read_bytes(&self) -> u64 { + self.read_bytes + } + + pub(crate) fn write_bytes(&self) -> u64 { + self.write_bytes + } +} diff --git a/src/app/data_harvester/disks/unix.rs b/src/app/data_harvester/disks/unix.rs new file mode 100644 index 00000000..93668b7c --- /dev/null +++ b/src/app/data_harvester/disks/unix.rs @@ -0,0 +1,73 @@ +//! Disk stats for Unix-like systems that aren't supported through other means. Officially, +//! for now, this means Linux and macOS. + +mod file_systems; + +use file_systems::*; + +mod usage; +use usage::*; + +cfg_if::cfg_if! { + if #[cfg(target_os = "linux")] { + mod linux; + pub use linux::*; + } else if #[cfg(target_os = "macos")] { + mod other; + use other::*; + + mod macos; + pub use macos::*; + } else { + mod other; + use other::*; + } +} + +use super::{keep_disk_entry, DiskHarvest}; +use crate::app::Filter; + +/// Returns the disk usage of the mounted (and for now, physical) disks. +pub fn get_disk_usage( + disk_filter: &Option, mount_filter: &Option, +) -> anyhow::Result> { + let mut vec_disks: Vec = Vec::new(); + + for partition in physical_partitions()? { + let name = partition.get_device_name(); + let mount_point = partition.mount_point().to_string_lossy().to_string(); + + // Precedence ordering in the case where name and mount filters disagree, "allow" takes precedence over "deny". + // + // For implementation, we do this as follows: + // 1. Is the entry allowed through any filter? That is, does it match an entry in a filter where `is_list_ignored` is `false`? If so, we always keep this entry. + // 2. Is the entry denied through any filter? That is, does it match an entry in a filter where `is_list_ignored` is `true`? If so, we always deny this entry. + // 3. Anything else is allowed. + + if keep_disk_entry(&name, &mount_point, disk_filter, mount_filter) { + // The usage line can fail in some cases (for example, if you use Void Linux + LUKS, + // see https://github.com/ClementTsang/bottom/issues/419 for details). + if let Ok(usage) = partition.usage() { + let total = usage.total(); + + vec_disks.push(DiskHarvest { + free_space: Some(usage.free()), + used_space: Some(total - usage.available()), + total_space: Some(total), + mount_point, + name, + }); + } else { + vec_disks.push(DiskHarvest { + free_space: None, + used_space: None, + total_space: None, + mount_point, + name, + }); + } + } + } + + Ok(vec_disks) +} diff --git a/src/app/data_harvester/disks/unix/file_systems.rs b/src/app/data_harvester/disks/unix/file_systems.rs new file mode 100644 index 00000000..91f26081 --- /dev/null +++ b/src/app/data_harvester/disks/unix/file_systems.rs @@ -0,0 +1,141 @@ +use std::str::FromStr; + +/// Known filesystems. From [heim](https://github.com/heim-rs/heim/blob/master/heim-disk/src/filesystem.rs). +/// +/// All physical filesystems should have their own enum element and all virtual filesystems will go into +/// the [`FileSystem::Other`] element. +#[derive(Debug, Eq, PartialEq, Hash, Clone)] +#[non_exhaustive] +pub enum FileSystem { + /// ext2 (https://en.wikipedia.org/wiki/Ext2) + Ext2, + + /// ext3 (https://en.wikipedia.org/wiki/Ext3) + Ext3, + + /// ext4 (https://en.wikipedia.org/wiki/Ext4) + Ext4, + + /// FAT (https://en.wikipedia.org/wiki/File_Allocation_Table) + VFat, + + /// exFAT (https://en.wikipedia.org/wiki/ExFAT) + ExFat, + + /// F2FS (https://en.wikipedia.org/wiki/F2FS) + F2fs, + + /// NTFS (https://en.wikipedia.org/wiki/NTFS) + Ntfs, + + /// ZFS (https://en.wikipedia.org/wiki/ZFS) + Zfs, + + /// HFS (https://en.wikipedia.org/wiki/Hierarchical_File_System) + Hfs, + + /// HFS+ (https://en.wikipedia.org/wiki/HFS_Plus) + HfsPlus, + + /// JFS (https://en.wikipedia.org/wiki/JFS_(file_system)) + Jfs, + + /// ReiserFS 3 (https://en.wikipedia.org/wiki/ReiserFS) + Reiser3, + + /// ReiserFS 4 (https://en.wikipedia.org/wiki/Reiser4) + Reiser4, + + /// Btrfs (https://en.wikipedia.org/wiki/Btrfs) + Btrfs, + + /// MINIX FS (https://en.wikipedia.org/wiki/MINIX_file_system) + Minix, + + /// NILFS (https://en.wikipedia.org/wiki/NILFS) + Nilfs, + + /// XFS (https://en.wikipedia.org/wiki/XFS) + Xfs, + + /// APFS (https://en.wikipedia.org/wiki/Apple_File_System) + Apfs, + + // TODO: Should it be considered as a physical FS? + /// FUSE (https://en.wikipedia.org/wiki/Filesystem_in_Userspace) + FuseBlk, + + // TODO: Extend list + /// Some unspecified filesystem. + Other(String), +} + +impl FileSystem { + /// Checks if filesystem is used for a physical devices. + #[inline] + pub fn is_physical(&self) -> bool { + !self.is_virtual() + } + + /// Checks if filesystem is used for a virtual devices (such as `tmpfs` or `smb` mounts). + #[inline] + pub fn is_virtual(&self) -> bool { + matches!(self, FileSystem::Other(..)) + } + + #[allow(dead_code)] + /// Returns a string identifying this filesystem. + pub fn as_str(&self) -> &str { + match self { + FileSystem::Ext2 => "ext2", + FileSystem::Ext3 => "ext3", + FileSystem::Ext4 => "ext4", + FileSystem::VFat => "vfat", + FileSystem::Ntfs => "ntfs", + FileSystem::Zfs => "zfs", + FileSystem::Hfs => "hfs", + FileSystem::Reiser3 => "reiserfs", + FileSystem::Reiser4 => "reiser4", + FileSystem::FuseBlk => "fuseblk", + FileSystem::ExFat => "exfat", + FileSystem::F2fs => "f2fs", + FileSystem::HfsPlus => "hfs+", + FileSystem::Jfs => "jfs", + FileSystem::Btrfs => "btrfs", + FileSystem::Minix => "minix", + FileSystem::Nilfs => "nilfs", + FileSystem::Xfs => "xfs", + FileSystem::Apfs => "apfs", + FileSystem::Other(string) => string.as_str(), + } + } +} + +impl FromStr for FileSystem { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + match () { + _ if s.eq_ignore_ascii_case("ext2") => Ok(FileSystem::Ext2), + _ if s.eq_ignore_ascii_case("ext3") => Ok(FileSystem::Ext3), + _ if s.eq_ignore_ascii_case("ext4") => Ok(FileSystem::Ext4), + _ if s.eq_ignore_ascii_case("vfat") => Ok(FileSystem::VFat), + _ if s.eq_ignore_ascii_case("ntfs") => Ok(FileSystem::Ntfs), + _ if s.eq_ignore_ascii_case("zfs") => Ok(FileSystem::Zfs), + _ if s.eq_ignore_ascii_case("hfs") => Ok(FileSystem::Hfs), + _ if s.eq_ignore_ascii_case("reiserfs") => Ok(FileSystem::Reiser3), + _ if s.eq_ignore_ascii_case("reiser4") => Ok(FileSystem::Reiser4), + _ if s.eq_ignore_ascii_case("exfat") => Ok(FileSystem::ExFat), + _ if s.eq_ignore_ascii_case("f2fs") => Ok(FileSystem::F2fs), + _ if s.eq_ignore_ascii_case("hfsplus") => Ok(FileSystem::HfsPlus), + _ if s.eq_ignore_ascii_case("jfs") => Ok(FileSystem::Jfs), + _ if s.eq_ignore_ascii_case("btrfs") => Ok(FileSystem::Btrfs), + _ if s.eq_ignore_ascii_case("minix") => Ok(FileSystem::Minix), + _ if s.eq_ignore_ascii_case("nilfs") => Ok(FileSystem::Nilfs), + _ if s.eq_ignore_ascii_case("xfs") => Ok(FileSystem::Xfs), + _ if s.eq_ignore_ascii_case("apfs") => Ok(FileSystem::Apfs), + _ if s.eq_ignore_ascii_case("fuseblk") => Ok(FileSystem::FuseBlk), + _ => Ok(FileSystem::Other(s.to_string())), + } + } +} diff --git a/src/app/data_harvester/disks/unix/linux/counters.rs b/src/app/data_harvester/disks/unix/linux/counters.rs new file mode 100644 index 00000000..22134b85 --- /dev/null +++ b/src/app/data_harvester/disks/unix/linux/counters.rs @@ -0,0 +1,85 @@ +//! Based on [heim's implementation](https://github.com/heim-rs/heim/blob/master/heim-disk/src/sys/linux/counters.rs). + +use std::{ + fs::File, + io::{self, BufRead, BufReader}, + num::ParseIntError, + str::FromStr, +}; + +use crate::app::data_harvester::disks::IoCounters; + +/// Copied from the `psutil` sources: +/// +/// "man iostat" states that sectors are equivalent with blocks and have +/// a size of 512 bytes. Despite this value can be queried at runtime +/// via /sys/block/{DISK}/queue/hw_sector_size and results may vary +/// between 1k, 2k, or 4k... 512 appears to be a magic constant used +/// throughout Linux source code: +/// * https://stackoverflow.com/a/38136179/376587 +/// * https://lists.gt.net/linux/kernel/2241060 +/// * https://github.com/giampaolo/psutil/issues/1305 +/// * https://github.com/torvalds/linux/blob/4f671fe2f9523a1ea206f63fe60a7c7b3a56d5c7/include/linux/bio.h#L99 +/// * https://lkml.org/lkml/2015/8/17/234 +const DISK_SECTOR_SIZE: u64 = 512; + +impl FromStr for IoCounters { + type Err = anyhow::Error; + + /// Converts a `&str` to an [`IoStats`]. + /// + /// Follows the format used in Linux 2.6+. Note that this completely ignores the following stats: + /// - Discard stats from 4.18+ + /// - Flush stats from 5.5+ + /// + /// https://www.kernel.org/doc/Documentation/iostats.txt + /// https://www.kernel.org/doc/Documentation/ABI/testing/procfs-diskstats + fn from_str(s: &str) -> anyhow::Result { + fn next_part<'a>(iter: &mut impl Iterator) -> Result<&'a str, io::Error> { + iter.next() + .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidData)) + } + + fn next_part_to_u64<'a>(iter: &mut impl Iterator) -> anyhow::Result { + next_part(iter)? + .parse() + .map_err(|err: ParseIntError| err.into()) + } + + // Skip the major and minor numbers. + let mut parts = s.split_whitespace().skip(2); + + let name = next_part(&mut parts)?.to_string(); + + // Skip read count, read merged count. + let mut parts = parts.skip(2); + let read_bytes = next_part_to_u64(&mut parts)? * DISK_SECTOR_SIZE; + + // Skip read time seconds, write count, and write merged count. + let mut parts = parts.skip(3); + let write_bytes = next_part_to_u64(&mut parts)? * DISK_SECTOR_SIZE; + + Ok(IoCounters::new(name, read_bytes, write_bytes)) + } +} + +/// Returns an iterator of disk I/O stats. Pulls data from `/proc/diskstats`. +pub fn io_stats() -> anyhow::Result>> { + const PROC_DISKSTATS: &str = "/proc/diskstats"; + + let mut results = vec![]; + let mut reader = BufReader::new(File::open(PROC_DISKSTATS)?); + let mut line = String::new(); + + // This saves us from doing a string allocation on each iteration compared to `lines()`. + while let Ok(bytes) = reader.read_line(&mut line) { + if bytes > 0 { + results.push(IoCounters::from_str(&line)); + line.clear(); + } else { + break; + } + } + + Ok(results) +} diff --git a/src/app/data_harvester/disks/unix/linux/mod.rs b/src/app/data_harvester/disks/unix/linux/mod.rs new file mode 100644 index 00000000..40a83267 --- /dev/null +++ b/src/app/data_harvester/disks/unix/linux/mod.rs @@ -0,0 +1,5 @@ +mod partition; +pub(crate) use partition::*; + +mod counters; +pub use counters::*; diff --git a/src/app/data_harvester/disks/unix/linux/partition.rs b/src/app/data_harvester/disks/unix/linux/partition.rs new file mode 100644 index 00000000..0e215747 --- /dev/null +++ b/src/app/data_harvester/disks/unix/linux/partition.rs @@ -0,0 +1,197 @@ +//! Implementation based on [heim's](https://github.com/heim-rs/heim) +//! Unix disk usage. + +use std::{ + ffi::CString, + fs::File, + io::{self, BufRead, BufReader}, + mem, + path::{Path, PathBuf}, + str::FromStr, +}; + +use anyhow::bail; + +use crate::app::data_harvester::disks::unix::{FileSystem, Usage}; + +/// Representation of partition details. Based on [`heim`](https://github.com/heim-rs/heim/tree/master). +pub(crate) struct Partition { + device: Option, + mount_point: PathBuf, + fs_type: FileSystem, +} + +impl Partition { + /// Returns the device name, if there is one. + #[inline] + pub fn device(&self) -> Option<&str> { + self.device.as_deref() + } + + /// Returns the mount point for this partition. + #[inline] + pub fn mount_point(&self) -> &Path { + self.mount_point.as_path() + } + + /// Returns the [`FileSystem`] of this partition. + #[inline] + pub fn fs_type(&self) -> &FileSystem { + &self.fs_type + } + + /// Returns the device name for the partition. + pub fn get_device_name(&self) -> String { + if let Some(device) = self.device() { + // See if this disk is actually mounted elsewhere on Linux. This is a workaround properly map I/O + // in some cases (i.e. disk encryption, https://github.com/ClementTsang/bottom/issues/419). + if let Ok(path) = std::fs::read_link(device) { + if path.is_absolute() { + path.into_os_string() + .into_string() + .unwrap_or_else(|_| "Name Unavailable".to_string()) + } else { + let mut combined_path = std::path::PathBuf::new(); + combined_path.push(device); + combined_path.pop(); // Pop the current file... + combined_path.push(path); + + if let Ok(canon_path) = std::fs::canonicalize(combined_path) { + // Resolve the local path into an absolute one... + canon_path + .into_os_string() + .into_string() + .unwrap_or_else(|_| "Name Unavailable".to_string()) + } else { + device.to_owned() + } + } + } else { + device.to_owned() + } + } else { + "Name Unavailable".to_string() + } + } + + /// Returns the usage stats for this partition. + pub fn usage(&self) -> anyhow::Result { + let path = self + .mount_point + .to_str() + .ok_or_else(|| io::Error::from(io::ErrorKind::InvalidInput)) + .and_then(|string| { + CString::new(string).map_err(|_| io::Error::from(io::ErrorKind::InvalidInput)) + }) + .map_err(|e| anyhow::anyhow!("invalid path: {e:?}"))?; + + let mut vfs = mem::MaybeUninit::::uninit(); + + // SAFETY: libc call, `path` is a valid C string and buf is a valid pointer to write to. + let result = unsafe { libc::statvfs(path.as_ptr(), vfs.as_mut_ptr()) }; + + if result == 0 { + // SAFETY: If result is 0, it succeeded, and vfs should be non-null. + let vfs = unsafe { vfs.assume_init() }; + Ok(Usage::new(vfs)) + } else { + Err(anyhow::anyhow!( + "statvfs had an issue getting info from {path:?}" + )) + } + } +} + +impl FromStr for Partition { + type Err = anyhow::Error; + + fn from_str(line: &str) -> anyhow::Result { + // Example: `/dev/sda3 /home ext4 rw,relatime,data=ordered 0 0` + let mut parts = line.splitn(5, ' '); + + let device = match parts.next() { + Some(device) if device == "none" => None, + Some(device) => Some(device.to_string()), + None => { + bail!("missing device"); + } + }; + let mount_point = match parts.next() { + Some(point) => PathBuf::from(point), + None => { + bail!("missing mount point"); + } + }; + let fs_type = match parts.next() { + Some(fs) => FileSystem::from_str(fs)?, + _ => { + bail!("missing filesystem type"); + } + }; + + // let options = match parts.next() { + // Some(opts) => opts.to_string(), + // None => { + // bail!("missing options"); + // } + // }; + + Ok(Partition { + device, + mount_point, + fs_type, + }) + } +} + +#[allow(dead_code)] +/// Returns a [`Vec`] containing all partitions. +pub(crate) fn partitions() -> anyhow::Result> { + const PROC_MOUNTS: &str = "/proc/mounts"; + + let mut results = vec![]; + let mut reader = BufReader::new(File::open(PROC_MOUNTS)?); + let mut line = String::new(); + + // This saves us from doing a string allocation on each iteration compared to `lines()`. + while let Ok(bytes) = reader.read_line(&mut line) { + if bytes > 0 { + if let Ok(partition) = Partition::from_str(&line) { + results.push(partition); + } + + line.clear(); + } else { + break; + } + } + + Ok(results) +} + +/// Returns a [`Vec`] containing all *physical* partitions. This is defined by +/// [`FileSystem::is_physical()`]. +pub(crate) fn physical_partitions() -> anyhow::Result> { + const PROC_MOUNTS: &str = "/proc/mounts"; + + let mut results = vec![]; + let mut reader = BufReader::new(File::open(PROC_MOUNTS)?); + let mut line = String::new(); + + // This saves us from doing a string allocation on each iteration compared to `lines()`. + while let Ok(bytes) = reader.read_line(&mut line) { + if bytes > 0 { + if let Ok(partition) = Partition::from_str(&line) { + if partition.fs_type().is_physical() { + results.push(partition); + } + } + + line.clear(); + } else { + break; + } + } + + Ok(results) +} diff --git a/src/app/data_harvester/disks/unix/macos/counters.rs b/src/app/data_harvester/disks/unix/macos/counters.rs new file mode 100644 index 00000000..9f31da38 --- /dev/null +++ b/src/app/data_harvester/disks/unix/macos/counters.rs @@ -0,0 +1,49 @@ +//! Based on [heim's implementation](https://github.com/heim-rs/heim/blob/master/heim-disk/src/sys/macos/counters.rs). + +use super::io_kit::{self, get_dict, get_disks, get_i64, get_string}; +use crate::app::data_harvester::disks::IoCounters; + +fn get_device_io(device: io_kit::IoObject) -> anyhow::Result { + let parent = device.service_parent()?; + + // XXX: Re: Conform check being disabled. + // + // Okay, so this is weird. + // + // The problem is that if I have this check - this is what sources like psutil use, for + // example (see https://github.com/giampaolo/psutil/blob/7eadee31db2f038763a3a6f978db1ea76bbc4674/psutil/_psutil_osx.c#LL1422C20-L1422C20) + // then this will only return stuff like disk0. + // + // The problem with this is that there is *never* a disk0 *disk* entry to correspond to this, + // so there will be entries like disk1 or whatnot. Someone's done some digging on the gopsutil + // repo (https://github.com/shirou/gopsutil/issues/855#issuecomment-610016435), and it seems + // like this is a consequence of how Apple does logical volumes. + // + // So with all that said, what I've found is that I *can* still get a mapping - but I have + // to disable the conform check, which... is weird. I'm not sure if this is valid at all. But + // it *does* seem to match Activity Monitor with regards to disk activity, so... I guess we + // can leave this for now...? + + // if !parent.conforms_to_block_storage_driver() { + // anyhow::bail!("{parent:?}, the parent of {device:?} does not conform to IOBlockStorageDriver") + // } + + let disk_props = device.properties()?; + let parent_props = parent.properties()?; + + let name = get_string(&disk_props, "BSD Name")?; + let stats = get_dict(&parent_props, "Statistics")?; + + let read_bytes = get_i64(&stats, "Bytes (Read)")? as u64; + let write_bytes = get_i64(&stats, "Bytes (Write)")? as u64; + + // let read_count = stats.get_i64("Operations (Read)")? as u64; + // let write_count = stats.get_i64("Operations (Write)")? as u64; + + Ok(IoCounters::new(name, read_bytes, write_bytes)) +} + +/// Returns an iterator of disk I/O stats. Pulls data through IOKit. +pub fn io_stats() -> anyhow::Result>> { + Ok(get_disks()?.map(get_device_io).collect()) +} diff --git a/src/app/data_harvester/disks/unix/macos/io_kit.rs b/src/app/data_harvester/disks/unix/macos/io_kit.rs new file mode 100644 index 00000000..42193fbd --- /dev/null +++ b/src/app/data_harvester/disks/unix/macos/io_kit.rs @@ -0,0 +1,10 @@ +mod bindings; + +mod io_iterator; +pub use io_iterator::*; + +mod io_object; +pub use io_object::*; + +mod io_disks; +pub use io_disks::get_disks; diff --git a/src/app/data_harvester/disks/unix/macos/io_kit/bindings.rs b/src/app/data_harvester/disks/unix/macos/io_kit/bindings.rs new file mode 100644 index 00000000..bb586b31 --- /dev/null +++ b/src/app/data_harvester/disks/unix/macos/io_kit/bindings.rs @@ -0,0 +1,59 @@ +//! C FFI bindings for [IOKit](https://developer.apple.com/documentation/iokit/). +//! +//! Based on [heim](https://github.com/heim-rs/heim/blob/master/heim-common/src/sys/macos/iokit/io_master_port.rs) +//! and [sysinfo's implementation](https://github.com/GuillaumeGomez/sysinfo/blob/master/src/apple/macos/ffi.rs). +//! +//! Ideally, we can remove this if sysinfo ever gains disk I/O capabilities. + +use core_foundation::base::{mach_port_t, CFAllocatorRef}; +use core_foundation::dictionary::CFMutableDictionaryRef; + +use libc::c_char; +use mach2::kern_return::kern_return_t; +use mach2::port::MACH_PORT_NULL; + +#[allow(non_camel_case_types)] +pub type io_object_t = mach_port_t; + +#[allow(non_camel_case_types)] +pub type io_iterator_t = io_object_t; +#[allow(non_camel_case_types)] +pub type io_registry_entry_t = io_object_t; + +pub type IOOptionBits = u32; + +/// See https://github.com/1kc/librazermacos/pull/27#issuecomment-1042368531. +#[allow(non_upper_case_globals)] +pub const kIOMasterPortDefault: mach_port_t = MACH_PORT_NULL; + +#[allow(non_upper_case_globals)] +pub const kIOServicePlane: &str = "IOService\0"; + +#[allow(non_upper_case_globals)] +pub const kIOMediaClass: &str = "IOMedia\0"; + +// See [here](https://developer.apple.com/documentation/iokit) for more details. +extern "C" { + + pub fn IOServiceGetMatchingServices( + mainPort: mach_port_t, matching: CFMutableDictionaryRef, existing: *mut io_iterator_t, + ) -> kern_return_t; + + pub fn IOServiceMatching(name: *const c_char) -> CFMutableDictionaryRef; + + pub fn IOIteratorNext(iterator: io_iterator_t) -> io_object_t; + + pub fn IOObjectRelease(obj: io_object_t) -> kern_return_t; + + pub fn IORegistryEntryGetParentEntry( + entry: io_registry_entry_t, plane: *const libc::c_char, parent: *mut io_registry_entry_t, + ) -> kern_return_t; + + // pub fn IOObjectConformsTo(object: io_object_t, className: *const libc::c_char) -> mach2::boolean::boolean_t; + + pub fn IORegistryEntryCreateCFProperties( + entry: io_registry_entry_t, properties: *mut CFMutableDictionaryRef, + allocator: CFAllocatorRef, options: IOOptionBits, + ) -> kern_return_t; + +} diff --git a/src/app/data_harvester/disks/unix/macos/io_kit/io_disks.rs b/src/app/data_harvester/disks/unix/macos/io_kit/io_disks.rs new file mode 100644 index 00000000..3d206973 --- /dev/null +++ b/src/app/data_harvester/disks/unix/macos/io_kit/io_disks.rs @@ -0,0 +1,24 @@ +use anyhow::bail; + +use mach2::kern_return; + +use super::{bindings::*, IoIterator}; + +pub fn get_disks() -> anyhow::Result { + let mut media_iter: io_iterator_t = 0; + + // SAFETY: This is a safe syscall via IOKit, all the arguments should be safe. + let result = unsafe { + IOServiceGetMatchingServices( + kIOMasterPortDefault, + IOServiceMatching(kIOMediaClass.as_ptr().cast()), + &mut media_iter, + ) + }; + + if result == kern_return::KERN_SUCCESS { + Ok(media_iter.into()) + } else { + bail!("IOServiceGetMatchingServices failed, error code {result}"); + } +} diff --git a/src/app/data_harvester/disks/unix/macos/io_kit/io_iterator.rs b/src/app/data_harvester/disks/unix/macos/io_kit/io_iterator.rs new file mode 100644 index 00000000..1f440466 --- /dev/null +++ b/src/app/data_harvester/disks/unix/macos/io_kit/io_iterator.rs @@ -0,0 +1,54 @@ +//! Based on [heim's](https://github.com/heim-rs/heim/blob/master/heim-common/src/sys/macos/iokit/io_iterator.rs). +//! implementation. + +use std::ops::{Deref, DerefMut}; + +use mach2::kern_return; + +use super::{bindings::*, io_object::IoObject}; + +/// Safe wrapper around the IOKit `io_iterator_t` type. +#[derive(Debug)] +pub struct IoIterator(io_iterator_t); + +impl From for IoIterator { + fn from(iter: io_iterator_t) -> IoIterator { + IoIterator(iter) + } +} + +impl Deref for IoIterator { + type Target = io_iterator_t; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for IoIterator { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Iterator for IoIterator { + type Item = IoObject; + + fn next(&mut self) -> Option { + // Basically, we just stop when we hit 0. + + // SAFETY: IOKit call, the passed argument (an `io_iterator_t`) is what is expected. + match unsafe { IOIteratorNext(self.0) } { + 0 => None, + io_object => Some(IoObject::from(io_object)), + } + } +} + +impl Drop for IoIterator { + fn drop(&mut self) { + // SAFETY: IOKit call, the passed argument (an `io_iterator_t`) is what is expected. + let result = unsafe { IOObjectRelease(self.0) }; + assert_eq!(result, kern_return::KERN_SUCCESS); + } +} diff --git a/src/app/data_harvester/disks/unix/macos/io_kit/io_object.rs b/src/app/data_harvester/disks/unix/macos/io_kit/io_object.rs new file mode 100644 index 00000000..0d567a86 --- /dev/null +++ b/src/app/data_harvester/disks/unix/macos/io_kit/io_object.rs @@ -0,0 +1,140 @@ +//! Based on [heim's](https://github.com/heim-rs/heim/blob/master/heim-common/src/sys/macos/iokit/io_object.rs) +//! implementation. + +use std::mem; + +use anyhow::{anyhow, bail}; +use core_foundation::base::{kCFAllocatorDefault, CFType, TCFType, ToVoid}; +use core_foundation::dictionary::{ + CFDictionary, CFDictionaryGetTypeID, CFDictionaryRef, CFMutableDictionary, + CFMutableDictionaryRef, +}; +use core_foundation::number::{CFNumber, CFNumberGetTypeID}; +use core_foundation::string::{CFString, CFStringGetTypeID}; +use mach2::kern_return; + +use super::bindings::*; + +/// Safe wrapper around the IOKit `io_object_t` type. +#[derive(Debug)] +pub struct IoObject(io_object_t); + +impl IoObject { + /// Returns a typed dictionary with this object's properties. + pub fn properties(&self) -> anyhow::Result> { + // SAFETY: The IOKit call should be fine, the arguments are safe. The `assume_init` should also be fine, as + // we guard against it with a check against `result` to ensure it succeeded. + unsafe { + let mut props = mem::MaybeUninit::::uninit(); + + let result = IORegistryEntryCreateCFProperties( + self.0, + props.as_mut_ptr(), + kCFAllocatorDefault, + 0, + ); + + if result != kern_return::KERN_SUCCESS { + bail!("IORegistryEntryCreateCFProperties failed, error code {result}.") + } else { + let props = props.assume_init(); + Ok(CFMutableDictionary::wrap_under_create_rule(props).to_immutable()) + } + } + } + + /// Gets the [`kIOServicePlane`] parent [`io_object_t`] for this [`io_object_t`], if there + /// is one. + pub fn service_parent(&self) -> anyhow::Result { + let mut parent: io_registry_entry_t = 0; + + // SAFETY: IOKit call, the arguments should be safe. + let result = unsafe { + IORegistryEntryGetParentEntry(self.0, kIOServicePlane.as_ptr().cast(), &mut parent) + }; + + if result != kern_return::KERN_SUCCESS { + bail!("IORegistryEntryGetParentEntry failed, error code {result}.") + } else { + Ok(parent.into()) + } + } + + // pub fn conforms_to_block_storage_driver(&self) -> bool { + // // SAFETY: IOKit call, the arguments should be safe. + // let result = + // unsafe { IOObjectConformsTo(self.0, "IOBlockStorageDriver\0".as_ptr().cast()) }; + + // result != 0 + // } +} + +impl From for IoObject { + fn from(obj: io_object_t) -> IoObject { + IoObject(obj) + } +} + +impl Drop for IoObject { + fn drop(&mut self) { + // SAFETY: IOKit call, the argument here (an `io_object_t`) should be safe and expected. + let result = unsafe { IOObjectRelease(self.0) }; + assert_eq!(result, kern_return::KERN_SUCCESS); + } +} + +pub fn get_dict( + dict: &CFDictionary, raw_key: &'static str, +) -> anyhow::Result> { + let key = CFString::from_static_string(raw_key); + + dict.find(&key) + .map(|value_ref| { + // SAFETY: Only used for debug asserts, system API call that should be safe. + unsafe { + debug_assert!(value_ref.type_of() == CFDictionaryGetTypeID()); + } + + // "Casting" `CFDictionary<*const void, *const void>` into a needed dict type + let ptr = value_ref.to_void() as CFDictionaryRef; + + // SAFETY: System API call, it should be safe? + unsafe { CFDictionary::wrap_under_get_rule(ptr) } + }) + .ok_or_else(|| anyhow!("missing key")) +} + +pub fn get_i64( + dict: &CFDictionary, raw_key: &'static str, +) -> anyhow::Result { + let key = CFString::from_static_string(raw_key); + + dict.find(&key) + .and_then(|value_ref| { + // SAFETY: Only used for debug asserts, system API call that should be safe. + unsafe { + debug_assert!(value_ref.type_of() == CFNumberGetTypeID()); + } + value_ref.downcast::() + }) + .and_then(|number| number.to_i64()) + .ok_or_else(|| anyhow!("missing key")) +} + +pub fn get_string( + dict: &CFDictionary, raw_key: &'static str, +) -> anyhow::Result { + let key = CFString::from_static_string(raw_key); + + dict.find(&key) + .and_then(|value_ref| { + // SAFETY: Only used for debug asserts, system API call that should be safe. + unsafe { + debug_assert!(value_ref.type_of() == CFStringGetTypeID()); + } + + value_ref.downcast::() + }) + .map(|cf_string| cf_string.to_string()) + .ok_or_else(|| anyhow!("missing key")) +} diff --git a/src/app/data_harvester/disks/unix/macos/mod.rs b/src/app/data_harvester/disks/unix/macos/mod.rs new file mode 100644 index 00000000..97d7e154 --- /dev/null +++ b/src/app/data_harvester/disks/unix/macos/mod.rs @@ -0,0 +1,4 @@ +mod counters; +pub use counters::*; + +mod io_kit; diff --git a/src/app/data_harvester/disks/unix/other/bindings.rs b/src/app/data_harvester/disks/unix/other/bindings.rs new file mode 100644 index 00000000..0dd4b4fb --- /dev/null +++ b/src/app/data_harvester/disks/unix/other/bindings.rs @@ -0,0 +1,46 @@ +//! Based on [heim's](https://github.com/heim-rs/heim/blob/master/heim-disk/src/sys/unix/bindings/mod.rs) +//! implementation. + +use std::io::Error; + +const MNT_NOWAIT: libc::c_int = 2; + +extern "C" { + fn getfsstat64(buf: *mut libc::statfs, bufsize: libc::c_int, flags: libc::c_int) + -> libc::c_int; +} + +/// Returns all the mounts on the system at the moment. +pub(crate) fn mounts() -> anyhow::Result> { + // SAFETY: System API FFI call, arguments should be correct. + let expected_len = unsafe { getfsstat64(std::ptr::null_mut(), 0, MNT_NOWAIT) }; + + let mut mounts: Vec = Vec::with_capacity(expected_len as usize); + + // SAFETY: System API FFI call, arguments should be correct. + let result = unsafe { + getfsstat64( + mounts.as_mut_ptr(), + std::mem::size_of::() as libc::c_int * expected_len, + MNT_NOWAIT, + ) + }; + + if result == -1 { + Err(anyhow::Error::from(Error::last_os_error()).context("getfsstat64")) + } else { + debug_assert_eq!( + expected_len, result, + "Expected {expected_len} statfs entries, but instead got {result} entries", + ); + + // SAFETY: We have a debug assert check, and if `result` is not correct (-1), we check against it. + // Otherwise, getfsstat64 should return the number of statfs structures if it succeeded. + // + // Source: https://man.freebsd.org/cgi/man.cgi?query=getfsstat&sektion=2&format=html + unsafe { + mounts.set_len(result as usize); + } + Ok(mounts) + } +} diff --git a/src/app/data_harvester/disks/unix/other/mod.rs b/src/app/data_harvester/disks/unix/other/mod.rs new file mode 100644 index 00000000..645d7c3f --- /dev/null +++ b/src/app/data_harvester/disks/unix/other/mod.rs @@ -0,0 +1,4 @@ +mod bindings; +mod partition; + +pub(crate) use partition::*; diff --git a/src/app/data_harvester/disks/unix/other/partition.rs b/src/app/data_harvester/disks/unix/other/partition.rs new file mode 100644 index 00000000..341c5126 --- /dev/null +++ b/src/app/data_harvester/disks/unix/other/partition.rs @@ -0,0 +1,99 @@ +use std::{ + ffi::{CStr, CString}, + os::unix::prelude::OsStrExt, + path::{Path, PathBuf}, + str::FromStr, +}; + +use anyhow::bail; + +use super::bindings; +use crate::app::data_harvester::disks::unix::{FileSystem, Usage}; + +pub(crate) struct Partition { + device: String, + mount_point: PathBuf, + fs_type: FileSystem, +} + +impl Partition { + /// Returns the mount point for this partition. + #[inline] + pub fn mount_point(&self) -> &Path { + self.mount_point.as_path() + } + + /// Returns the [`FileSystem`] of this partition. + #[inline] + pub fn fs_type(&self) -> &FileSystem { + &self.fs_type + } + + /// Returns the usage stats for this partition. + pub fn usage(&self) -> anyhow::Result { + let path = CString::new(self.mount_point().as_os_str().as_bytes())?; + let mut vfs = std::mem::MaybeUninit::::uninit(); + + // SAFETY: System API call. Arguments should be correct. + let result = unsafe { libc::statvfs(path.as_ptr(), vfs.as_mut_ptr()) }; + + if result == 0 { + // SAFETY: We check that it succeeded (result is 0), which means vfs should be populated. + Ok(Usage::new(unsafe { vfs.assume_init() })) + } else { + bail!("statvfs failed to get the disk usage for disk {path:?}") + } + } + + /// Returns the device name. + #[inline] + pub fn get_device_name(&self) -> String { + self.device.clone() + } +} + +fn partitions_iter() -> anyhow::Result> { + let mounts = bindings::mounts()?; + + unsafe fn ptr_to_cow<'a>(ptr: *const i8) -> std::borrow::Cow<'a, str> { + CStr::from_ptr(ptr).to_string_lossy() + } + + Ok(mounts.into_iter().map(|stat| { + // SAFETY: Should be a non-null pointer. + let device = unsafe { ptr_to_cow(stat.f_mntfromname.as_ptr()).to_string() }; + + let fs_type = { + // SAFETY: Should be a non-null pointer. + let fs_type_str = unsafe { ptr_to_cow(stat.f_fstypename.as_ptr()) }; + FileSystem::from_str(&fs_type_str).unwrap_or(FileSystem::Other(fs_type_str.to_string())) + }; + + let mount_point = { + // SAFETY: Should be a non-null pointer. + let path_str = unsafe { ptr_to_cow(stat.f_mntonname.as_ptr()).to_string() }; + PathBuf::from(path_str) + }; + + Partition { + device, + mount_point, + fs_type, + } + })) +} + +#[allow(dead_code)] +/// Returns a [`Vec`] containing all partitions. +pub(crate) fn partitions() -> anyhow::Result> { + partitions_iter().map(|iter| iter.collect()) +} + +/// Returns a [`Vec`] containing all *physical* partitions. This is defined by +/// [`FileSystem::is_physical()`]. +pub(crate) fn physical_partitions() -> anyhow::Result> { + partitions_iter().map(|iter| { + iter.filter(|partition| partition.fs_type().is_physical()) + .collect() + }) +} diff --git a/src/app/data_harvester/disks/unix/usage.rs b/src/app/data_harvester/disks/unix/usage.rs new file mode 100644 index 00000000..d9e75323 --- /dev/null +++ b/src/app/data_harvester/disks/unix/usage.rs @@ -0,0 +1,32 @@ +pub struct Usage(libc::statvfs); + +// Note that x86 returns `u32` values while x86-64 returns `u64`s, so we convert everything +// to `u64` for consistency. +#[allow(clippy::useless_conversion)] +impl Usage { + pub(crate) fn new(vfs: libc::statvfs) -> Self { + Self(vfs) + } + + /// Returns the total number of bytes available. + pub fn total(&self) -> u64 { + u64::from(self.0.f_blocks) * u64::from(self.0.f_frsize) + } + + /// Returns the available number of bytes used. Note this is not necessarily the same as [`free`]. + pub fn available(&self) -> u64 { + u64::from(self.0.f_bfree) * u64::from(self.0.f_frsize) + } + + #[allow(dead_code)] + /// Returns the total number of bytes used. Equal to `total - available` on Unix. + pub fn used(&self) -> u64 { + let avail_to_root = u64::from(self.0.f_bfree) * u64::from(self.0.f_frsize); + self.total() - avail_to_root + } + + /// Returns the total number of bytes free. Note this is not necessarily the same as [`available`]. + pub fn free(&self) -> u64 { + u64::from(self.0.f_bavail) * u64::from(self.0.f_frsize) + } +} diff --git a/src/app/data_harvester/disks/windows.rs b/src/app/data_harvester/disks/windows.rs new file mode 100644 index 00000000..b5708494 --- /dev/null +++ b/src/app/data_harvester/disks/windows.rs @@ -0,0 +1,75 @@ +//! Disk stats via sysinfo. + +use itertools::Itertools; +use sysinfo::{DiskExt, System, SystemExt}; + +use super::{keep_disk_entry, DiskHarvest}; +use crate::app::data_harvester::disks::IoCounters; +use crate::app::filter::Filter; + +mod bindings; +use bindings::*; + +/// Returns I/O stats. +pub(crate) fn io_stats() -> anyhow::Result>> { + let volume_io = all_volume_io()?; + + Ok(volume_io + .into_iter() + .map_ok(|(performance, volume_name)| { + let name = volume_name; + let read_bytes = performance.BytesRead as u64; + let write_bytes = performance.BytesWritten as u64; + + IoCounters::new(name, read_bytes, write_bytes) + }) + .collect::>()) +} + +pub(crate) fn get_disk_usage( + sys: &System, disk_filter: &Option, mount_filter: &Option, +) -> Vec { + let disks = sys.disks(); + disks + .iter() + .filter_map(|disk| { + let name = { + let name = disk.name(); + + if name.is_empty() { + "No Name".to_string() + } else { + name.to_os_string() + .into_string() + .unwrap_or_else(|_| "Name Unavailable".to_string()) + } + }; + + let mount_point = disk + .mount_point() + .as_os_str() + .to_os_string() + .into_string() + .unwrap_or_else(|_| "Mount Unavailable".to_string()); + + let volume_name = volume_name_from_mount(&mount_point).ok(); + + if keep_disk_entry(&name, &mount_point, disk_filter, mount_filter) { + let free_space = disk.available_space(); + let total_space = disk.total_space(); + let used_space = total_space - free_space; + + Some(DiskHarvest { + name, + mount_point, + volume_name, + free_space: Some(free_space), + used_space: Some(used_space), + total_space: Some(total_space), + }) + } else { + None + } + }) + .collect() +} diff --git a/src/app/data_harvester/disks/windows/bindings.rs b/src/app/data_harvester/disks/windows/bindings.rs new file mode 100644 index 00000000..247c6b6d --- /dev/null +++ b/src/app/data_harvester/disks/windows/bindings.rs @@ -0,0 +1,198 @@ +//! Windows bindings to get disk I/O counters. + +use std::{ + ffi::OsString, + io, mem, + os::windows::prelude::{OsStrExt, OsStringExt}, + path::{Path, PathBuf}, +}; + +use anyhow::bail; +use windows::Win32::{ + Foundation::{self, CloseHandle}, + Storage::FileSystem::{ + CreateFileW, FindFirstVolumeW, FindNextVolumeW, FindVolumeClose, FindVolumeHandle, + GetVolumeNameForVolumeMountPointW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_READ, + FILE_SHARE_WRITE, OPEN_EXISTING, + }, + System::{ + Ioctl::{DISK_PERFORMANCE, IOCTL_DISK_PERFORMANCE}, + IO::DeviceIoControl, + }, +}; + +/// Returns the I/O for a given volume. +/// +/// Based on [psutil's implementation](https://github.com/giampaolo/psutil/blob/52fe5517f716dedf9c9918e56325e49a49146130/psutil/arch/windows/disk.c#L78-L83) +/// and [heim's implementation](https://github.com/heim-rs/heim/blob/master/heim-disk/src/sys/windows/bindings/perf.rs). +fn volume_io(volume: &Path) -> anyhow::Result { + if volume.is_file() { + // We assume the volume is a directory, so bail ASAP if it isn't. + bail!("Expects a directory to be passed in."); + } + + let volume = { + let mut wide_path = volume.as_os_str().encode_wide().collect::>(); + + // We replace the trailing backslash and replace it with a \0. + wide_path.pop(); + wide_path.push(0x0000); + + wide_path + }; + + // SAFETY: API call, arguments should be correct. We must also check after the call to ensure it is valid. + let h_device = unsafe { + CreateFileW( + windows::core::PCWSTR(volume.as_ptr()), + 0, + FILE_SHARE_READ | FILE_SHARE_WRITE, + None, + OPEN_EXISTING, + FILE_FLAGS_AND_ATTRIBUTES(0), + Foundation::HANDLE::default(), + )? + }; + + if h_device.is_invalid() { + bail!("Invalid handle value: {:?}", io::Error::last_os_error()); + } + + let mut disk_performance = DISK_PERFORMANCE::default(); + let mut bytes_returned = 0; + + // SAFETY: This should be safe, we'll manually check the results and the arguments should be valid. + let ret = unsafe { + DeviceIoControl( + h_device, + IOCTL_DISK_PERFORMANCE, + None, + 0, + Some(&mut disk_performance as *mut _ as _), + mem::size_of::() as u32, + Some(&mut bytes_returned), + None, + ) + }; + + // SAFETY: This should be safe, we will check the result as well. + let handle_result = unsafe { CloseHandle(h_device) }; + if !handle_result.as_bool() { + const ERROR_INVALID_FUNCTION: i32 = Foundation::ERROR_INVALID_FUNCTION.0 as i32; + const ERROR_NOT_SUPPORTED: i32 = Foundation::ERROR_NOT_SUPPORTED.0 as i32; + + match io::Error::last_os_error().raw_os_error() { + Some(ERROR_INVALID_FUNCTION) => { + bail!("Handle error: invalid function"); + } + Some(ERROR_NOT_SUPPORTED) => { + bail!("Handle error: not supported"); + } + _ => { + bail!( + "Unknown handle device result error: {:?}", + io::Error::last_os_error() + ); + } + } + } + + if !ret.as_bool() { + bail!("Device I/O error: {:?}", io::Error::last_os_error()); + } else { + Ok(disk_performance) + } +} + +fn current_volume(buffer: &[u16]) -> PathBuf { + let first_null = buffer.iter().position(|byte| *byte == 0x00).unwrap_or(0); + let path_string = OsString::from_wide(&buffer[..first_null]); + + PathBuf::from(path_string) +} + +fn close_find_handle(handle: FindVolumeHandle) -> anyhow::Result<()> { + // Clean up the handle. + // SAFETY: This should be safe, we will check the result as well. + let handle_result = unsafe { FindVolumeClose(handle) }; + if !handle_result.as_bool() { + bail!("Could not close volume handle."); + } else { + Ok(()) + } +} + +/// Returns the I/O for all volumes. +/// +/// Based on [psutil's implementation](https://github.com/giampaolo/psutil/blob/52fe5517f716dedf9c9918e56325e49a49146130/psutil/arch/windows/disk.c#L78-L83) +/// and [heim's implementation](https://github.com/heim-rs/heim/blob/master/heim-disk/src/sys/windows/bindings/perf.rs). +pub(crate) fn all_volume_io() -> anyhow::Result>> { + const ERROR_NO_MORE_FILES: i32 = Foundation::ERROR_NO_MORE_FILES.0 as i32; + let mut ret = vec![]; + let mut buffer = [0_u16; Foundation::MAX_PATH as usize]; + + // Get the first volume and add the stats needed. + // SAFETY: We must verify the handle is correct. If no volume is found, it will be set to `INVALID_HANDLE_VALUE`. + let handle = unsafe { FindFirstVolumeW(&mut buffer) }?; + if handle.is_invalid() { + bail!("Invalid handle value: {:?}", io::Error::last_os_error()); + } + + { + let volume = current_volume(&buffer); + ret.push(volume_io(&volume).map(|res| (res, volume.to_string_lossy().to_string()))); + } + + // Now iterate until there are no more volumes. + while unsafe { FindNextVolumeW(handle, &mut buffer) }.as_bool() { + let volume = current_volume(&buffer); + ret.push(volume_io(&volume).map(|res| (res, volume.to_string_lossy().to_string()))); + } + + let err = io::Error::last_os_error(); + match err.raw_os_error() { + // Iteration completed successfully, continue on. + Some(ERROR_NO_MORE_FILES) => {} + // Some error occured. + _ => { + close_find_handle(handle)?; + bail!("Error while iterating over volumes: {err:?}"); + } + } + + close_find_handle(handle)?; + + Ok(ret) +} + +/// Returns the volume name from a mount name if possible. +pub(crate) fn volume_name_from_mount(mount: &str) -> anyhow::Result { + // According to winapi docs 50 is a reasonable length to accomodate the volume path + // https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-getvolumenameforvolumemountpointw + const VOLUME_MAX_LEN: usize = 50; + + let mount = { + let mount_path = Path::new(mount); + let mut wide_path = mount_path.as_os_str().encode_wide().collect::>(); + + // Always push on a \0 character, without this it will occasionally break. + wide_path.push(0x0000); + + wide_path + }; + let mut buffer = [0_u16; VOLUME_MAX_LEN]; + + // SAFETY: API call, we must check the result for validating safety. + let result = unsafe { + GetVolumeNameForVolumeMountPointW(windows::core::PCWSTR(mount.as_ptr()), &mut buffer) + }; + + if !result.as_bool() { + bail!( + "Could not get volume name for mount point: {:?}", + io::Error::last_os_error() + ); + } else { + Ok(current_volume(&buffer).to_string_lossy().to_string()) + } +} diff --git a/src/app/data_harvester/memory/arc.rs b/src/app/data_harvester/memory/arc.rs index 9684c8be..e5342c9b 100644 --- a/src/app/data_harvester/memory/arc.rs +++ b/src/app/data_harvester/memory/arc.rs @@ -3,7 +3,7 @@ use super::MemHarvest; /// Return ARC usage. #[cfg(feature = "zfs")] pub(crate) fn get_arc_usage() -> Option { - let (mem_total_in_kib, mem_used_in_kib) = { + let (mem_total, mem_used) = { cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { // TODO: [OPT] is this efficient? @@ -64,12 +64,12 @@ pub(crate) fn get_arc_usage() -> Option { }; Some(MemHarvest { - total_bytes: mem_total_in_kib, - used_bytes: mem_used_in_kib, - use_percent: if mem_total_in_kib == 0 { + total_bytes: mem_total, + used_bytes: mem_used, + use_percent: if mem_total == 0 { None } else { - Some(mem_used_in_kib as f64 / mem_total_in_kib as f64 * 100.0) + Some(mem_used as f64 / mem_total as f64 * 100.0) }, }) } diff --git a/src/app/data_harvester/temperature/linux.rs b/src/app/data_harvester/temperature/linux.rs index 53e64a17..8e5f7674 100644 --- a/src/app/data_harvester/temperature/linux.rs +++ b/src/app/data_harvester/temperature/linux.rs @@ -31,8 +31,8 @@ fn get_from_hwmon( let mut temperature_vec: Vec = vec![]; let path = Path::new("/sys/class/hwmon"); - // NOTE: Technically none of this is async, *but* sysfs is in memory, - // so in theory none of this should block if we're slightly careful. + // Note that none of this is async if we ever go back to it, but sysfs is in + // memory, so in theory none of this should block if we're slightly careful. // Of note is that reading the temperature sensors of a device that has // `/sys/class/hwmon/hwmon*/device/power_state` == `D3cold` will // wake the device up, and will block until it initializes. diff --git a/src/app/filter.rs b/src/app/filter.rs index 10629d94..22c08e29 100644 --- a/src/app/filter.rs +++ b/src/app/filter.rs @@ -9,7 +9,7 @@ impl Filter { /// Whether the filter should keep the entry or reject it. #[inline] pub(crate) fn keep_entry(&self, value: &str) -> bool { - if self.list.iter().any(|regex| regex.is_match(value)) { + if self.has_match(value) { // If a match is found, then if we wanted to ignore if we match, return false. If we want // to keep if we match, return true. Thus, return the inverse of `is_list_ignored`. !self.is_list_ignored @@ -17,6 +17,12 @@ impl Filter { self.is_list_ignored } } + + /// Whether there is a filter that matches the result. + #[inline] + pub(crate) fn has_match(&self, value: &str) -> bool { + self.list.iter().any(|regex| regex.is_match(value)) + } } #[cfg(test)] diff --git a/src/bin/main.rs b/src/bin/main.rs index bbe6d3d2..330b4639 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -299,11 +299,8 @@ fn main() -> Result<()> { } // I think doing it in this order is safe... - *thread_termination_lock.lock().unwrap() = true; - thread_termination_cvar.notify_all(); - cleanup_terminal(&mut terminal)?; Ok(()) diff --git a/src/data_conversion.rs b/src/data_conversion.rs index 56cd4d19..9d246085 100644 --- a/src/data_conversion.rs +++ b/src/data_conversion.rs @@ -97,6 +97,7 @@ impl ConvertedData { .iter() .zip(&data.io_labels) .for_each(|(disk, (io_read, io_write))| { + // Because this sometimes does *not* equal to disk.total. let summed_total_bytes = match (disk.used_space, disk.free_space) { (Some(used), Some(free)) => Some(used + free), _ => None, diff --git a/src/lib.rs b/src/lib.rs index c2e9b60b..4bfae9da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -533,8 +533,7 @@ pub fn create_collection_thread( } } - // TODO: [OPT] this feels like it might not be totally optimal. Hm. - futures::executor::block_on(data_state.update_data()); + data_state.update_data(); // Yet another check to bail if needed... if let Ok(is_terminated) = termination_ctrl_lock.try_lock() { diff --git a/src/utils/error.rs b/src/utils/error.rs index 5b95e19a..3764469c 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -14,9 +14,6 @@ pub enum BottomError { /// An error when there is an IO exception. #[error("IO exception, {0}")] InvalidIo(String), - /// An error when the heim library encounters a problem. - #[error("Error caused by Heim, {0}")] - InvalidHeim(String), /// An error when the Crossterm library encounters a problem. #[error("Error caused by Crossterm, {0}")] CrosstermError(String), @@ -50,13 +47,6 @@ impl From for BottomError { } } -#[cfg(not(target_os = "freebsd"))] -impl From for BottomError { - fn from(err: heim::Error) -> Self { - BottomError::InvalidHeim(err.to_string()) - } -} - impl From for BottomError { fn from(err: std::num::ParseIntError) -> Self { BottomError::ConfigError(err.to_string()) diff --git a/src/widgets/disk_table.rs b/src/widgets/disk_table.rs index 494c01b5..1d6b32d9 100644 --- a/src/widgets/disk_table.rs +++ b/src/widgets/disk_table.rs @@ -78,7 +78,11 @@ impl DiskWidgetData { if let (Some(used_bytes), Some(summed_total_bytes)) = (self.used_bytes, self.summed_total_bytes) { - Some(used_bytes as f64 / summed_total_bytes as f64 * 100_f64) + if summed_total_bytes > 0 { + Some(used_bytes as f64 / summed_total_bytes as f64 * 100_f64) + } else { + None + } } else { None }