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
This commit is contained in:
Clement Tsang 2023-04-10 05:52:46 -04:00 committed by GitHub
parent b2801b16a9
commit 9edde9b133
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 1622 additions and 764 deletions

View File

@ -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

460
Cargo.lock generated
View File

@ -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"

View File

@ -1,6 +1,6 @@
[package]
name = "bottom"
version = "0.8.1"
version = "0.9.0"
authors = ["Clement Tsang <cjhtsang@uwaterloo.ca>"]
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",
] }

View File

@ -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<disks::DiskHarvest>, 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<Regex> = 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<Regex> =
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 {

View File

@ -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")]

View File

@ -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<String>,
// TODO: Maybe unify all these?
pub free_space: Option<u64>,
pub used_space: Option<u64>,
pub total_space: Option<u64>,
@ -28,4 +43,137 @@ pub struct IoData {
pub write_bytes: u64,
}
pub type IoHarvest = std::collections::HashMap<String, Option<IoData>>;
pub type IoHarvest = HashMap<String, Option<IoData>>;
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<IoHarvest> {
let mut io_hash: HashMap<String, Option<IoData>> = 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<Filter>, mount_filter: &Option<Filter>,
) -> 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<Filter>, mount_filter: &Option<Filter>) -> Vec<usize> {
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]);
}
}

View File

@ -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<Option<IoHarvest>> {
if !actually_get {
return Ok(None);
}
pub fn get_io_usage() -> error::Result<IoHarvest> {
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<Opt
.map(|disk| (disk.name, None))
.collect()
})?;
Ok(Some(io_harvest))
Ok(io_harvest)
}
pub async fn get_disk_usage(
actually_get: bool, disk_filter: &Option<Filter>, mount_filter: &Option<Filter>,
) -> crate::utils::error::Result<Option<Vec<DiskHarvest>>> {
if !actually_get {
return Ok(None);
}
pub fn get_disk_usage(
disk_filter: &Option<Filter>, mount_filter: &Option<Filter>,
) -> error::Result<Vec<DiskHarvest>> {
let vec_disks: Vec<DiskHarvest> = 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<Filter>, &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<Filter>, &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<StorageSystemInformation> {

View File

@ -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<Option<IoHarvest>> {
if !actually_get {
return Ok(None);
}
use futures::StreamExt;
let mut io_hash: std::collections::HashMap<String, Option<IoData>> =
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::<heim::units::information::byte>(),
write_bytes: io.write_bytes().get::<heim::units::information::byte>(),
}),
);
}
}
Ok(Some(io_hash))
}
pub async fn get_disk_usage(
actually_get: bool, disk_filter: &Option<Filter>, mount_filter: &Option<Filter>,
) -> crate::utils::error::Result<Option<Vec<DiskHarvest>>> {
if !actually_get {
return Ok(None);
}
use futures::StreamExt;
let mut vec_disks: Vec<DiskHarvest> = 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::<heim::units::information::byte>()),
used_space: Some(usage.used().get::<heim::units::information::byte>()),
total_space: Some(usage.total().get::<heim::units::information::byte>()),
mount_point,
name,
});
} else {
vec_disks.push(DiskHarvest {
free_space: None,
used_space: None,
total_space: None,
mount_point,
name,
});
}
}
}
}
Ok(Some(vec_disks))
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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<Filter>, mount_filter: &Option<Filter>,
) -> anyhow::Result<Vec<DiskHarvest>> {
let mut vec_disks: Vec<DiskHarvest> = 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)
}

View File

@ -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<Self> {
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())),
}
}
}

View File

@ -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<IoCounters> {
fn next_part<'a>(iter: &mut impl Iterator<Item = &'a str>) -> 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<Item = &'a str>) -> anyhow::Result<u64> {
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<Vec<anyhow::Result<IoCounters>>> {
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)
}

View File

@ -0,0 +1,5 @@
mod partition;
pub(crate) use partition::*;
mod counters;
pub use counters::*;

View File

@ -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<String>,
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<Usage> {
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::<libc::statvfs>::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<Partition> {
// 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<Vec<Partition>> {
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<Vec<Partition>> {
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)
}

View File

@ -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<IoCounters> {
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<Vec<anyhow::Result<IoCounters>>> {
Ok(get_disks()?.map(get_device_io).collect())
}

View File

@ -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;

View File

@ -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;
}

View File

@ -0,0 +1,24 @@
use anyhow::bail;
use mach2::kern_return;
use super::{bindings::*, IoIterator};
pub fn get_disks() -> anyhow::Result<IoIterator> {
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}");
}
}

View File

@ -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<io_iterator_t> 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<Self::Item> {
// 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);
}
}

View File

@ -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<CFDictionary<CFString, CFType>> {
// 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::<CFMutableDictionaryRef>::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<IoObject> {
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<io_object_t> 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<CFString, CFType>, raw_key: &'static str,
) -> anyhow::Result<CFDictionary<CFString, CFType>> {
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<CFString, CFType>, raw_key: &'static str,
) -> anyhow::Result<i64> {
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::<CFNumber>()
})
.and_then(|number| number.to_i64())
.ok_or_else(|| anyhow!("missing key"))
}
pub fn get_string(
dict: &CFDictionary<CFString, CFType>, raw_key: &'static str,
) -> anyhow::Result<String> {
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::<CFString>()
})
.map(|cf_string| cf_string.to_string())
.ok_or_else(|| anyhow!("missing key"))
}

View File

@ -0,0 +1,4 @@
mod counters;
pub use counters::*;
mod io_kit;

View File

@ -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<Vec<libc::statfs>> {
// 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<libc::statfs> = 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::<libc::statfs>() 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)
}
}

View File

@ -0,0 +1,4 @@
mod bindings;
mod partition;
pub(crate) use partition::*;

View File

@ -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<Usage> {
let path = CString::new(self.mount_point().as_os_str().as_bytes())?;
let mut vfs = std::mem::MaybeUninit::<libc::statvfs>::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<impl Iterator<Item = Partition>> {
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<Vec<Partition>> {
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<Vec<Partition>> {
partitions_iter().map(|iter| {
iter.filter(|partition| partition.fs_type().is_physical())
.collect()
})
}

View File

@ -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)
}
}

View File

@ -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<Vec<anyhow::Result<IoCounters>>> {
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::<Vec<_>>())
}
pub(crate) fn get_disk_usage(
sys: &System, disk_filter: &Option<Filter>, mount_filter: &Option<Filter>,
) -> Vec<DiskHarvest> {
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()
}

View File

@ -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<DISK_PERFORMANCE> {
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::<Vec<_>>();
// 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::<DISK_PERFORMANCE>() 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<Vec<anyhow::Result<(DISK_PERFORMANCE, String)>>> {
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<String> {
// 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::<Vec<_>>();
// 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())
}
}

View File

@ -3,7 +3,7 @@ use super::MemHarvest;
/// Return ARC usage.
#[cfg(feature = "zfs")]
pub(crate) fn get_arc_usage() -> Option<MemHarvest> {
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<MemHarvest> {
};
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)
},
})
}

View File

@ -31,8 +31,8 @@ fn get_from_hwmon(
let mut temperature_vec: Vec<TempHarvest> = 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.

View File

@ -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)]

View File

@ -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(())

View File

@ -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,

View File

@ -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() {

View File

@ -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<std::io::Error> for BottomError {
}
}
#[cfg(not(target_os = "freebsd"))]
impl From<heim::Error> for BottomError {
fn from(err: heim::Error) -> Self {
BottomError::InvalidHeim(err.to_string())
}
}
impl From<std::num::ParseIntError> for BottomError {
fn from(err: std::num::ParseIntError) -> Self {
BottomError::ConfigError(err.to_string())

View File

@ -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
}