diff --git a/.github/workflows/regression.yml b/.github/workflows/regression.yml index d30c0f52d..859af1a52 100644 --- a/.github/workflows/regression.yml +++ b/.github/workflows/regression.yml @@ -17,9 +17,12 @@ jobs: - os: ubuntu-24.04-arm rust: stable target: aarch64-unknown-linux-gnu - - os: macOS-latest + - os: macos-15-intel rust: stable target: x86_64-apple-darwin + - os: macos-latest + rust: stable + target: aarch64-apple-darwin - os: windows-latest rust: stable target: x86_64-pc-windows-msvc diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6e0e193ce..08dd7a946 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: - os: [ubuntu-latest, ubuntu-24.04-arm, macOS-latest, windows-latest, windows-11-arm] + os: [ubuntu-latest, ubuntu-24.04-arm, macos-15-intel, macos-latest, windows-latest, windows-11-arm] rust: [stable] runs-on: ${{ matrix.os }} @@ -32,11 +32,14 @@ jobs: run: | rustup target add aarch64-unknown-linux-musl sudo apt-get -qq install musl-tools + - name: Setup x86_64 mac + if: matrix.os == 'macos-15-intel' + run: | + rustup target add x86_64-apple-darwin - name: Setup aarch64 mac - if: matrix.os == 'macOS-latest' + if: matrix.os == 'macos-latest' run: | rustup target add aarch64-apple-darwin - rustup target add x86_64-apple-darwin - name: Build for linux if: matrix.os == 'ubuntu-latest' run: | @@ -46,9 +49,12 @@ jobs: - name: Build for linux aarch64 if: matrix.os == 'ubuntu-24.04-arm' run: make release_lnx_aarch64 - - name: Build for macOS - if: matrix.os == 'macOS-latest' - run: make release_mac + - name: Build for macOS x86_64 + if: matrix.os == 'macos-15-intel' + run: make release_mac_x86_64 + - name: Build for macOS aarch64 + if: matrix.os == 'macos-latest' + run: make release_mac_aarch64 - name: Build for Windows if: matrix.os == 'windows-latest' run: make release_win diff --git a/CHANGELOG.md b/CHANGELOG.md index 91689abd0..f7840632e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,18 +4,26 @@ * [Added] Arch column for Windows (process image architecture) * [Added] Real Command column for Windows (command line arguments). This column was printing pure file name, which is now moved to FileName column. -* [Added] Priority column for Windows (user-mode priority class) * [Added] RtPriority column for Windows (kernel base priority) * [Added] Threads column for Windows (thread count) * [Added] Session column for Windows (session ID) * [Added] State column for Windows (scheduler state derived from thread states) -* [Added] Env column for Windows (environment variables) +* [Added] Env column for Windows and macOS (environment variables) * [Added] RecvBytes and SendBytes columns for Windows (network I/O rate; needs Windows 11 or later) -* [Added] WorkDir column for Windows (current working directory) -* [Added] `--thread` support for Windows +* [Added] WorkDir column for Windows, macOS and FreeBSD (current working directory) +* [Added] FileName column for macOS (Process file name) +* [Added] `--thread` support for Windows, macOS and FreeBSD +* [Added] External pager support for Windows by `[pager] command` (built-in pager is used if it is not set) +* [Added] `show_user_only` of `[display]` section and `--user` (`-u`) option to show the processes of one user (a name, a uid, or a SID on Windows, as well as `"myself"` and `"all"`) * [Changed] Group and Gid columns for Windows read the process token only when the column is displayed +* [Changed] `--procfs` is now a Linux-only option; the other platforms reject it as an unknown argument +* [Fixed] System processes no longer being dropped silently on macOS * [Fixed] ReadBytes / WriteBytes divided by a mis-scaled interval (seconds added to milliseconds) * [Fixed] Fix invalid JSON output when a column is skipped by --only or --tree +* [Fixed] Pid recycled by the kernel no longer reported with a bogus rate +* [Fixed] ElapsedTime and StartTime columns no longer show a bogus value when the start time is unavailable +* [Fixed] Gid and Group columns for macOS now currectly report the effective group (matching the other platforms) +* [Fixed] Avoid silently omitting Linux processes when the open file limit is low ## [v0.14.12](https://github.com/dalance/procs/compare/v0.14.11...v0.14.12) - 2026-06-25 diff --git a/Cargo.lock b/Cargo.lock index c4c03fc36..cb1dcbe21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,31 +8,20 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -95,9 +84,9 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arrayvec" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02882884d3e1bc524fb12c79f107f6ad0e1cfd498c536ffb494301740995dfe" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" [[package]] name = "async-stream" @@ -118,18 +107,18 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.5", ] [[package]] @@ -144,45 +133,22 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "bindgen" -version = "0.65.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" -dependencies = [ - "bitflags 1.3.2", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "log", - "peeking_take_while", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex 1.3.0", - "syn 2.0.118", - "which 4.4.2", -] - [[package]] name = "bindgen" version = "0.72.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" dependencies = [ - "bitflags 2.13.0", + "bitflags", "cexpr", "clang-sys", "itertools", "proc-macro2", "quote", "regex", - "rustc-hash 2.1.2", + "rustc-hash", "shlex 1.3.0", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -202,33 +168,15 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" - -[[package]] -name = "bitvec" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcec3d12c579d40898fe0a9a358a803c23e9c52ca3c425707f81c9436211837" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "borsh" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f3f6da4992df95bbcd9af42a6c7dcb994498fc9048230405f3b36ff7cd3f145" +checksum = "553c5d846a6ba5150c65e3b1b8ec073bcf1abc20f9b7220de384a4443ea4e20a" dependencies = [ "borsh-derive", "bytes", @@ -237,35 +185,15 @@ dependencies = [ [[package]] name = "borsh-derive" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8fb4fb5740e4b2c4884ff95f5f32f5e8479db1e8fd8eb49ddbe09eb09bb7c" +checksum = "12cdfe656708a01f89b451a7d36466e6fe6c414de0aa18fc54f864f6f9ca9f56" dependencies = [ "once_cell", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.118", -] - -[[package]] -name = "bsd-kvm" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b4eb28b17b77a2974f6307a6f7f18c294d71543ceac0f021b89a4eb9e3ccbe" -dependencies = [ - "bsd-kvm-sys", - "libc", - "thiserror 1.0.69", -] - -[[package]] -name = "bsd-kvm-sys" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65408f926d3766661595309e0a70bbed02ef27bb74603db6199ab9394103c932" -dependencies = [ - "bindgen 0.65.1", + "syn 3.0.5", ] [[package]] @@ -286,28 +214,6 @@ dependencies = [ "utf8-width", ] -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "byteorder" version = "1.5.0" @@ -316,15 +222,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cc" -version = "1.2.65" +version = "1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" dependencies = [ "find-msvc-tools", "shlex 2.0.1", @@ -347,9 +253,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chrono" @@ -364,9 +270,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.8.1" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a" dependencies = [ "glob", "libc", @@ -397,9 +303,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.5" +version = "4.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772" +checksum = "3be2ad0423bdbbb0e25bc89add796f3559706d4a95e1bc98e4d9662a957b6a19" dependencies = [ "clap", ] @@ -413,7 +319,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.5", ] [[package]] @@ -467,27 +373,27 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.5.15" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.21" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crossterm" @@ -495,7 +401,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.13.0", + "bitflags", "crossterm_winapi", "mio", "parking_lot", @@ -511,7 +417,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "bitflags 2.13.0", + "bitflags", "crossterm_winapi", "derive_more", "document-features", @@ -557,7 +463,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -592,13 +498,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.5", ] [[package]] @@ -624,7 +530,7 @@ dependencies = [ "serde", "serde_json", "tar", - "thiserror 2.0.18", + "thiserror", "tokio", "tokio-stream", "tokio-util", @@ -654,9 +560,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.16.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "encode_unicode" @@ -724,18 +630,19 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", "miniz_oxide", + "zlib-rs", ] [[package]] @@ -762,17 +669,11 @@ dependencies = [ "futures-core", ] -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futures" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -784,9 +685,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -794,44 +695,44 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.5", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -866,19 +767,22 @@ dependencies = [ ] [[package]] -name = "glob" -version = "0.3.3" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] [[package]] -name = "hashbrown" -version = "0.12.3" +name = "glob" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "hashbrown" @@ -898,15 +802,6 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "home" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "http" version = "0.2.12" @@ -1003,9 +898,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -1017,9 +912,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -1030,9 +925,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1044,16 +939,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -1064,15 +960,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -1106,12 +1002,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", - "hashbrown 0.17.1", + "hashbrown", ] [[package]] @@ -1137,27 +1033,15 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ "cfg-if", "futures-util", "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" version = "0.2.189" @@ -1180,16 +1064,16 @@ version = "0.14.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a54ad7278b8bc5301d5ffd2a94251c004feb971feba96c971ea4063645990757" dependencies = [ - "bindgen 0.72.1", + "bindgen", "errno 0.3.14", "libc", ] [[package]] name = "libredox" -version = "0.1.17" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" dependencies = [ "libc", ] @@ -1208,9 +1092,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "litrs" @@ -1229,9 +1113,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "mach2" @@ -1241,9 +1125,9 @@ checksum = "b0d28e293f2b8c9d2b2d1c0193bd3c1cbdc0d2cf396888dc01162f7cf9d6c3f3" [[package]] name = "memchr" -version = "2.8.2" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "minimal-lexical" @@ -1253,9 +1137,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", @@ -1272,14 +1156,14 @@ dependencies = [ "parking_lot", "regex", "textwrap", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "mio" -version = "1.2.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "4b18443e9c262bfe8fa82f51666e2642c53393f7e5c27b3e1aeab922cff5b9d8" dependencies = [ "libc", "log", @@ -1310,7 +1194,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1328,7 +1212,7 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags 2.13.0", + "bitflags", "cfg-if", "cfg_aliases", "libc", @@ -1419,12 +1303,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "percent-encoding" version = "2.3.2" @@ -1448,7 +1326,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1459,9 +1337,9 @@ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -1507,16 +1385,6 @@ dependencies = [ "termtree", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.118", -] - [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -1528,9 +1396,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -1541,7 +1409,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" dependencies = [ - "bitflags 2.13.0", + "bitflags", "chrono", "flate2", "procfs-core", @@ -1554,7 +1422,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" dependencies = [ - "bitflags 2.13.0", + "bitflags", "chrono", "hex", ] @@ -1564,8 +1432,6 @@ name = "procs" version = "0.14.12" dependencies = [ "anyhow", - "bsd-kvm", - "bsd-kvm-sys", "byte-unit", "chrono", "clap", @@ -1593,54 +1459,44 @@ dependencies = [ "toml", "unicode-width", "uzers", - "which 8.0.6", + "which", "windows-sys 0.61.2", ] [[package]] -name = "ptr_meta" -version = "0.1.4" +name = "quote" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ - "ptr_meta_derive", + "proc-macro2", ] [[package]] -name = "ptr_meta_derive" -version = "0.1.4" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "quote" -version = "1.0.46" +name = "rand" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ - "proc-macro2", + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", ] -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" -version = "0.8.6" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ - "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.9.0", + "rand_core 0.9.5", ] [[package]] @@ -1650,7 +1506,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", ] [[package]] @@ -1659,7 +1525,16 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", ] [[package]] @@ -1668,7 +1543,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.13.0", + "bitflags", ] [[package]] @@ -1677,29 +1552,29 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.18", + "thiserror", ] [[package]] name = "ref-cast" -version = "1.0.25" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.25" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.5", ] [[package]] @@ -1716,9 +1591,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -1731,44 +1606,6 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - -[[package]] -name = "rkyv" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" -dependencies = [ - "bitvec", - "bytecheck", - "bytes", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "roff" version = "1.1.1" @@ -1777,16 +1614,16 @@ checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189" [[package]] name = "rust_decimal" -version = "1.42.1" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2a24f50780bc85f09cc6ac299bdf1424302742d77221106859c9d8b102126a" +checksum = "7653272e75dcac41dc199fbea6f5797633994fafd339943c06c9af16bf29cd3a" dependencies = [ "arrayvec", "borsh", "bytes", "num-traits", - "rand", - "rkyv", + "rand 0.8.8", + "rand 0.9.5", "serde", "serde_json", "wasm-bindgen", @@ -1794,15 +1631,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - -[[package]] -name = "rustc-hash" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustc_version" @@ -1819,7 +1650,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.13.0", + "bitflags", "errno 0.3.14", "libc", "linux-raw-sys 0.4.15", @@ -1832,7 +1663,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.0", + "bitflags", "errno 0.3.14", "libc", "linux-raw-sys 0.12.1", @@ -1841,15 +1672,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "schemars" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" dependencies = [ "dyn-clone", "ref-cast", @@ -1863,12 +1694,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "seahash" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" - [[package]] name = "semver" version = "1.0.28" @@ -1902,14 +1727,14 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.5", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -1972,15 +1797,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simplelog" @@ -2001,9 +1820,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" [[package]] name = "socket2" @@ -2017,9 +1836,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -2039,9 +1858,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" dependencies = [ "proc-macro2", "quote", @@ -2050,20 +1869,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -2078,15 +1886,9 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "tar" version = "0.4.46" @@ -2109,7 +1911,7 @@ dependencies = [ "mockall", "scopeguard", "simplelog", - "thiserror 2.0.18", + "thiserror", "winapi", ] @@ -2148,49 +1950,29 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.69" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", + "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.5", ] [[package]] name = "time" -version = "0.3.51" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "libc", @@ -2210,9 +1992,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.30" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -2220,29 +2002,14 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.53.1" @@ -2253,27 +2020,27 @@ dependencies = [ "libc", "mio", "pin-project-lite", - "socket2 0.6.4", + "socket2 0.6.5", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.5", ] [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -2282,9 +2049,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", @@ -2319,9 +2086,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.12+spec-1.1.0" +version = "0.25.13+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" dependencies = [ "indexmap", "toml_datetime", @@ -2407,9 +2174,9 @@ dependencies = [ [[package]] name = "utf8-width" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" +checksum = "159a7cadce548703edd50d24069bc294c5415ecab0a480e0cd1ca06d112dc94a" [[package]] name = "utf8_iter" @@ -2423,16 +2190,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "1.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "uzers" version = "0.12.2" @@ -2443,12 +2200,6 @@ dependencies = [ "log", ] -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "want" version = "0.3.1" @@ -2464,11 +2215,20 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -2479,9 +2239,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2489,38 +2249,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "which" version = "8.0.6" @@ -2582,7 +2330,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2593,7 +2341,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2713,27 +2461,24 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" dependencies = [ "memchr", ] [[package]] -name = "writeable" -version = "0.6.3" +name = "wit-bindgen" +version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] -name = "wyz" -version = "0.5.1" +name = "writeable" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "xattr" @@ -2764,28 +2509,28 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.52" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +checksum = "d35102a9f36d089ccae9e4c6802bc118be4487b80aaffc0ab4e0cf5ce92d2873" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.52" +version = "0.8.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +checksum = "146c01f5ab44258da43cf276c74a2763db2ff3969c9c652c3f2de07041d0b2bc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2805,15 +2550,15 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -2822,9 +2567,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -2833,17 +2578,23 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.5", ] +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml index d1eb3b6e8..39d02167a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,8 +81,6 @@ mach2 = "0.7.0" windows-sys = { version = "0.61", features = ["Wdk_System_SystemInformation", "Wdk_System_SystemServices", "Wdk_System_Threading", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_NetworkManagement_IpHelper", "Win32_Security", "Win32_System_Diagnostics_Debug", "Win32_System_Kernel", "Win32_System_Threading", "Win32_System_WindowsProgramming"] } [target.'cfg(target_os = "freebsd")'.dependencies] -bsd-kvm = "0.1.5" -bsd-kvm-sys = "0.2.0" pager = "0.16.1" uzers = "0.12" which = "8" diff --git a/Makefile b/Makefile index 66599b2be..a3c5df9d9 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ BIN_NAME = procs export LONG_VERSION -.PHONY: all test clean release_lnx release_win release_win_arm64 release_mac +.PHONY: all test clean release_lnx release_win release_win_arm64 release_mac release_mac_x86_64 release_mac_aarch64 all: test @@ -38,10 +38,14 @@ release_win_arm64: mv -v target/aarch64-pc-windows-msvc/release/${BIN_NAME}.exe ./ 7z a ${BIN_NAME}-v${VERSION}-aarch64-windows.zip ${BIN_NAME}.exe -release_mac: +release_mac: release_mac_x86_64 release_mac_aarch64 + +release_mac_x86_64: cargo build --locked --release --target=x86_64-apple-darwin - cargo build --locked --release --target=aarch64-apple-darwin zip -j ${BIN_NAME}-v${VERSION}-x86_64-mac.zip target/x86_64-apple-darwin/release/${BIN_NAME} + +release_mac_aarch64: + cargo build --locked --release --target=aarch64-apple-darwin zip -j ${BIN_NAME}-v${VERSION}-aarch64-mac.zip target/aarch64-apple-darwin/release/${BIN_NAME} release_rpm: diff --git a/README.md b/README.md index 4f5f57db9..ce4362842 100644 --- a/README.md +++ b/README.md @@ -30,15 +30,16 @@ - Pager support - Watch mode (like `top`) - Tree view +- Narrowing to the processes of the current effective user ## Platform -- Linux is supported. -- macOS is experimentally supported. - - macOS version is checked only in the GitHub Actions environment. - - The issues caused by real-machine are welcome. -- Windows is supported. -- FreeBSD is experimentally supported. +- Linux +- macOS +- Windows +- FreeBSD + +All tested on hosts. ## Installation @@ -243,7 +244,24 @@ Instead of them, built-in pager can be used by configuration `use_builtin`. #### Windows -On Windows, built-in pager is always used. +On Windows, the built-in pager is used by default. +If `command` of `[pager]` section is set, it is launched as an external pager +unless `use_builtin` is `true`, which asks for the built-in pager explicitly. + +```toml +[pager] +command = "less -SR" +``` + +The command is split by whitespaces, and a part surrounded by `"` or `'` is kept as a single +argument, so a path containing spaces should be quoted like +`"C:\Program Files\Git\usr\bin\less.exe" -SR`. +Quotes must be balanced. + +The command is launched directly without a shell, so shell metacharacters like a pipe or a +redirection are not available. Use `cmd /c "..."` if they are required. + +If the command cannot be parsed or launched, procs falls back to the built-in pager. ### Watch mode @@ -270,6 +288,29 @@ procs --tree If `TreeSlot` column exists in config, dependency tree is shown at the slot. +### Show the processes of one user + +By default `procs` shows the processes of every user. `show_user_only` in the +`[display]` section, or the `--user` (`-u`) option, narrows the listing down to +one of them. + +```bash +# Show the processes of the current user +procs -u +procs --user myself +procs --user $USER + +# Show the processes of the root user +procs -u root +procs --user root +procs --user 0 + +# Show processes of all users (default) +procs -u all +``` + +See [the section](#show_user_only) for the details. + ### Sort column Column sort order can be changed by `--sorta` or `--sortd` option. @@ -443,14 +484,14 @@ The first `[[columns]]` is shown at left side, and the last is shown at right si | Docker | -not supported- | Docker container name | o | o | | | | Eip | eip | Instruction pointer | o | | | | | ElapsedTime | -not supported- | Elapsed time | o | o | o | o | -| Env | `e` output modifier | Environment variables | o | | | o | +| Env | `e` output modifier | Environment variables | o | o | | o | | Esp | esp | Stack pointer | o | | | | -| FileName | comm | File name | o | | o | o | -| Gid | egid | Group ID | o | o | o | o | +| FileName | comm | File name | o | o | o | o | +| Gid | egid | Effective group ID | o | o | o | o | | GidFs | fgid | File system group ID | o | | | | | GidReal | rgid | Real group ID | o | o | | o | | GidSaved | sgid | Saved group ID | o | o | | o | -| Group | egroup | Group name | o | o | o | o | +| Group | egroup | Effective group name | o | o | o | o | | GroupFs | fgroup | File system group name | o | | | | | GroupReal | rgroup | Real group name | o | o | | o | | GroupSaved | sgroup | Saved group name | o | o | | o | @@ -513,7 +554,7 @@ The first `[[columns]]` is shown at left side, and the last is shown at right si | VmTotal | -not supported- | Total virtual memory size | *[^*] | o | *[^*] | *[^*] | | VoluntaryContextSw | -not supported- | Voluntary context switch count | o | | | o | | Wchan | wchan | Process sleeping kernel function | o | | | o | -| WorkDir | -not supported- | Current working directory | o | | o | | +| WorkDir | -not supported- | Current working directory | o | o | o | o | | WriteByte | -not supported- | Write bytes to storage | o | o | o | o | [^*]: Alias for VmRss on these platforms @@ -651,7 +692,7 @@ style = "223" # 223 for both theme | show_footer | true, false | false | Whether footer row is shown | | show_kthreads | true, false | true | Whether processes which belong to kthread are shown ( Linux only ) | | cut_to_terminal | true, false | true | Whether output lines are truncated for output into terminal | -| cut_to_pager | true, false | false | Whether output lines are truncated for output into pager | +| cut_to_pager | true, false | false | Whether output lines are truncated for output into pager (true for built-in pager) | | cut_to_pipe | true, false | false | Whether output lines are truncated for output into pipe | | color_mode | Auto, Always, Disable | Auto | The default behavior of output coloring without `--color` commandline option | | separator | [String] | │ | String used as Separator | @@ -660,6 +701,7 @@ style = "223" # 223 for both theme | tree_symbols | [String; 5] | [│, ─, ┬, ├, └] | Symbols used by tree view | | abbr_sid | true, false | true | Whether machine SID is abbreviated ( Windows only ) | | theme | Auto, Dark, Light | Auto | Default theme | +| show_user_only | "all", "myself", [User] | "all" | Which user's processes are shown | If `color_mode` is `Auto`, color is enabled for terminal and pager, disabled for pipe. @@ -681,6 +723,40 @@ If `abbr_sid` is `true`, SID is shown like below: S-1-5-21-...-1001 ``` +#### `show_user_only` + +By default `procs` shows the processes of every user. `show_user_only` names the +one user whose processes are kept, and `--user` (`-u`) sets it from the command +line: + +- `"all"`: every user's processes. This is the default, and what leaving the key + out means. +- `"myself"`: the processes of the user `procs` runs as. A bare `--user` (or + `-u`) is short for this. +- anything else: a user to keep, written as a name (`root`, `alice`), as a uid + in decimal (`1000`), or - on Windows - as a SID. + +The user is looked up while `procs` starts, and a name that matches no account +stops it with `no such user: ...` rather than leaving an empty listing behind. + +On Windows there is no uid, so a number is looked up as a name like anything +else and normally matches nothing. What the filter compares there is the whole +SID, so an account is kept only if it is that account. The `Uid` column shows +the SID to use - in full, which is what `abbr_sid` abbreviates. + +A process whose owner cannot be determined - a Windows protected process, or +some of the kernel's own - counts as *not* the wanted user and is dropped. +Running with more privileges (`sudo`, or an elevated prompt on Windows) makes +more owners readable and therefore keeps more processes. + +The filter is applied while the processes are collected, not afterwards, so +everything a platform would otherwise do per process - opening a handle and +reading a command line on Windows, the `/proc/` reads on Linux - is skipped +for the processes that are dropped. + +With `--tree`, the parents of a process that was dropped are not added back: the +process simply starts a tree of its own. + ### `[sort]` section @@ -713,4 +789,8 @@ If `column` is 0, value is sorted by the left column. | command | [Command] | less -SR | Pager command | If `mode` is `Auto`, pager is used only when output lines exceed terminal height. -Default pager is `less -SR` ( if `less` is not found, `more -f` ). +Default pager is `less -SR` on POSIX systems ( if `less` is not found, `more -f` ). + +On Windows, `command` is used if it is set and `use_builtin` is `false`, and the built-in pager +is used otherwise. +Note that the `PAGER` environment variable is not referenced on any platform. diff --git a/config/large.toml b/config/large.toml index 20845433a..64a4c2c38 100644 --- a/config/large.toml +++ b/config/large.toml @@ -194,6 +194,7 @@ ascending = "▲" descending = "▼" tree_symbols = ["│", "─", "┬", "├", "└"] abbr_sid = true +show_user_only = "all" [sort] column = 0 @@ -204,4 +205,3 @@ path = "unix:///var/run/docker.sock" [pager] mode = "Auto" - diff --git a/config/small.toml b/config/small.toml index e2815e7fa..0557f1260 100644 --- a/config/small.toml +++ b/config/small.toml @@ -108,6 +108,7 @@ ascending = "▲" descending = "▼" tree_symbols = ["│", "─", "┬", "├", "└"] abbr_sid = true +show_user_only = "all" [sort] column = 0 @@ -118,4 +119,3 @@ path = "unix:///var/run/docker.sock" [pager] mode = "Auto" - diff --git a/man/procs.1.adoc b/man/procs.1.adoc index 004d97f22..4c5b666cc 100644 --- a/man/procs.1.adoc +++ b/man/procs.1.adoc @@ -40,9 +40,23 @@ procs - a replacement for `ps` written in Rust *Show Docker container name*:: procs growi +*Show the processes of every user*:: procs + +*Show only the processes of the current user*:: procs --user myself + +*Show only the processes of one user*:: procs --user root + +== SHOW THE PROCESSES OF ONE USER + +By default *procs* shows the processes of every user. The `--user` (or `-u`) option, or `show_user_only` in the `[display]` section of the configuration file, narrows the listing down to one user: `"all"` for every user, `"myself"` for the user *procs* runs as, or a user to keep, written as a name, as a uid in decimal, or - on Windows - as a SID. + +The user is looked up while *procs* starts, and a name that matches no account stops it rather than leaving an empty listing behind. + +A process whose owner cannot be determined, such as a Windows protected process, counts as not the wanted user and is hidden. Running with more privileges makes more owners readable. + == PAGER -On Linux and macOS, `less` is the default pager. If not available, `more` is used. Built-in pager can be used by configuring `use_builtin`. On Windows, the built-in pager is always used. +On Linux and macOS, `less` is the default pager. If not available, `more` is used. Built-in pager can be used by configuring `use_builtin`. On Windows, the built-in pager is used unless `command` of the `[pager]` section is set, in which case that command is launched as an external pager. == WATCH MODE SHORTCUTS diff --git a/src/column.rs b/src/column.rs index 87b019b5a..ab97eae5c 100644 --- a/src/column.rs +++ b/src/column.rs @@ -19,12 +19,12 @@ pub trait Column { config: &Config, ) -> String; fn display_unit(&self, align: &ConfigColumnAlign) -> String; - fn display_content(&self, pid: i32, align: &ConfigColumnAlign) -> Option; - fn display_json(&self, pid: i32) -> String; - fn find_partial(&self, pid: i32, keyword: &str, content_to_lowercase: bool) -> bool; - fn find_exact(&self, pid: i32, keyword: &str, content_to_lowercase: bool) -> bool; - fn sorted_pid(&self, order: &ConfigSortOrder) -> Vec; - fn apply_visible(&mut self, visible_pids: &[i32]); + fn display_content(&self, pid: i64, align: &ConfigColumnAlign) -> Option; + fn display_json(&self, pid: i64) -> String; + fn find_partial(&self, pid: i64, keyword: &str, content_to_lowercase: bool) -> bool; + fn find_exact(&self, pid: i64, keyword: &str, content_to_lowercase: bool) -> bool; + fn sorted_pid(&self, order: &ConfigSortOrder) -> Vec; + fn apply_visible(&mut self, visible_pids: &[i64]); fn reset_width( &mut self, order: Option, @@ -32,7 +32,7 @@ pub trait Column { max_width: Option, min_width: Option, ); - fn update_width(&mut self, pid: i32, max_width: Option); + fn update_width(&mut self, pid: i64, max_width: Option); fn get_width(&self) -> usize; fn is_numeric(&self) -> bool; } @@ -77,7 +77,7 @@ macro_rules! column_default_display_content { () => { fn display_content( &self, - pid: i32, + pid: i64, align: &$crate::config::ConfigColumnAlign, ) -> Option { self.fmt_contents @@ -90,7 +90,7 @@ macro_rules! column_default_display_content { #[macro_export] macro_rules! column_default_display_json { () => { - fn display_json(&self, pid: i32) -> String { + fn display_json(&self, pid: i64) -> String { let value = if self.is_numeric() { self.raw_contents .get(&pid) @@ -114,7 +114,7 @@ macro_rules! column_default_display_json { #[macro_export] macro_rules! column_default_find_partial { () => { - fn find_partial(&self, pid: i32, keyword: &str, content_to_lowercase: bool) -> bool { + fn find_partial(&self, pid: i64, keyword: &str, content_to_lowercase: bool) -> bool { if let Some(content) = self.fmt_contents.get(&pid) { if content_to_lowercase { content.to_ascii_lowercase().find(keyword).is_some() @@ -131,7 +131,7 @@ macro_rules! column_default_find_partial { #[macro_export] macro_rules! column_default_find_exact { () => { - fn find_exact(&self, pid: i32, keyword: &str, content_to_lowercase: bool) -> bool { + fn find_exact(&self, pid: i64, keyword: &str, content_to_lowercase: bool) -> bool { if let Some(content) = self.fmt_contents.get(&pid) { if content_to_lowercase { content.to_ascii_lowercase() == keyword @@ -148,8 +148,8 @@ macro_rules! column_default_find_exact { #[macro_export] macro_rules! column_default_sorted_pid { ($x:ty) => { - fn sorted_pid(&self, order: &$crate::config::ConfigSortOrder) -> Vec { - let mut contents: Vec<(&i32, &$x)> = self.raw_contents.iter().collect(); + fn sorted_pid(&self, order: &$crate::config::ConfigSortOrder) -> Vec { + let mut contents: Vec<(&i64, &$x)> = self.raw_contents.iter().collect(); contents.sort_by_key(|&(_x, y)| y); if matches!(*order, $crate::config::ConfigSortOrder::Descending) { contents.reverse() @@ -162,7 +162,7 @@ macro_rules! column_default_sorted_pid { #[macro_export] macro_rules! column_default_apply_visible { () => { - fn apply_visible(&mut self, _visible_pids: &[i32]) {} + fn apply_visible(&mut self, _visible_pids: &[i64]) {} }; } @@ -206,7 +206,7 @@ macro_rules! column_default_reset_width { #[macro_export] macro_rules! column_default_update_width { () => { - fn update_width(&mut self, pid: i32, max_width: Option) { + fn update_width(&mut self, pid: i64, max_width: Option) { if let Some(content) = self.fmt_contents.get(&pid) { let content_len = unicode_width::UnicodeWidthStr::width(content.as_str()); self.width = cmp::max(content_len, self.width); @@ -239,13 +239,25 @@ macro_rules! column_default_is_numeric { #[macro_export] macro_rules! column_default { ($x:ty, $y:expr) => { + $crate::column_default_without_sorted_pid!($y); + $crate::column_default_sorted_pid!($x); + }; +} + +/// Everything [`column_default`] gives a column except `sorted_pid`. +/// +/// For a column that orders its rows itself: `Pid` sorts by the absolute +/// value of the key, because a thread row is keyed by its negated thread id +/// and would otherwise sort below every process. +#[macro_export] +macro_rules! column_default_without_sorted_pid { + ($y:expr) => { $crate::column_default_display_header!(); $crate::column_default_display_unit!(); $crate::column_default_display_content!(); $crate::column_default_display_json!(); $crate::column_default_find_partial!(); $crate::column_default_find_exact!(); - $crate::column_default_sorted_pid!($x); $crate::column_default_apply_visible!(); $crate::column_default_reset_width!(); $crate::column_default_update_width!(); diff --git a/src/columns/arch.rs b/src/columns/arch.rs index 6b241ed31..916d1d4cf 100644 --- a/src/columns/arch.rs +++ b/src/columns/arch.rs @@ -15,8 +15,8 @@ const CPU_TYPE_ARM64: i32 = 16777228; pub struct Arch { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -37,7 +37,15 @@ impl Arch { impl Column for Arch { fn add(&mut self, proc: &ProcessInfo) { let pid = proc.pid; - let arch = arch_from_pid(pid); + // An image belongs to a process, so a thread row is left blank: its + // key is a negated thread id the OS does not know - on macOS that id + // is 64 bit wide, so handing it to `sysctl` would truncate it and + // could name a different process. + let arch = if proc.pid < 0 { + "" + } else { + arch_from_pid(pid) + }; let fmt_content = arch.to_string(); let raw_content = fmt_content.clone(); @@ -101,13 +109,13 @@ mod machine { /// WOW64 processes report their own architecture, not the host's: a 32-bit /// process on an x86_64 machine yields `x86`. #[cfg(target_os = "windows")] -pub fn arch_from_pid(pid: i32) -> &'static str { +pub fn arch_from_pid(pid: i64) -> &'static str { use windows_sys::Win32::Foundation::{CloseHandle, FALSE, HANDLE}; use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION}; // 0 is the idle process and has no image; negative pids are not real. if pid <= 0 { - return "unknown"; + return ""; } // SAFETY: `pid` is only handed to `OpenProcess`, and the handle it hands @@ -160,7 +168,7 @@ fn arch_from_machine(machine: u16) -> &'static str { // --------------------------------------------------------------------------- #[cfg(target_os = "macos")] -pub fn arch_from_pid(pid: i32) -> &'static str { +pub fn arch_from_pid(pid: i64) -> &'static str { use { libc::{sysctl, sysctlnametomib, cpu_type_t, size_t, CTL_KERN, KERN_PROC, KERN_PROC_PID}, std::{mem, ffi::CString}, @@ -177,7 +185,7 @@ pub fn arch_from_pid(pid: i32) -> &'static str { return "unknown"; } - mib[length as usize] = pid; + mib[length as usize] = pid as libc::c_int; length += 1; if unsafe { sysctl(mib.as_mut_ptr(), length as u32, &mut cpu_type as *mut _ as *mut _, &mut size, core::ptr::null_mut(), 0) } != 0 { @@ -193,7 +201,7 @@ pub fn arch_from_pid(pid: i32) -> &'static str { mib[0] = CTL_KERN; mib[1] = KERN_PROC; mib[2] = KERN_PROC_PID; - mib[3] = pid; + mib[3] = pid as libc::c_int; length = 4; size = mem::size_of::(); diff --git a/src/columns/ccgroup.rs b/src/columns/ccgroup.rs index 507aac733..489d0eda4 100644 --- a/src/columns/ccgroup.rs +++ b/src/columns/ccgroup.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct Ccgroup { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, pat_user: Regex, pat_machine: Regex, diff --git a/src/columns/cgroup.rs b/src/columns/cgroup.rs index 30abcbe10..2570467dc 100644 --- a/src/columns/cgroup.rs +++ b/src/columns/cgroup.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Cgroup { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/command.rs b/src/columns/command.rs index d2b6f54d3..f0381799d 100644 --- a/src/columns/command.rs +++ b/src/columns/command.rs @@ -1,14 +1,65 @@ use crate::process::ProcessInfo; use crate::util::sanitize_control_chars; use crate::{column_default, Column}; +#[cfg(target_os = "freebsd")] +use libc::{CTL_KERN, KERN_PROC, KERN_PROC_ARGS, c_void}; use std::cmp; use std::collections::HashMap; +#[cfg(target_os = "freebsd")] +use std::ptr; + +#[cfg(target_os = "freebsd")] +pub(crate) fn get_process_args(pid: i64) -> Vec { + // A thread row carries its thread id negated, which is not a pid and + // names no process to ask about. `work_dir_of` guards the same way. + if pid <= 0 { + return Vec::new(); + } + + let mut mib = [CTL_KERN, KERN_PROC, KERN_PROC_ARGS, pid as i32]; + let mut size = 0usize; + if unsafe { + libc::sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + ptr::null_mut(), + &mut size, + ptr::null_mut(), + 0, + ) + } != 0 + || size == 0 + { + return Vec::new(); + } + + let mut bytes = vec![0u8; size]; + if unsafe { + libc::sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + bytes.as_mut_ptr() as *mut c_void, + &mut size, + ptr::null_mut(), + 0, + ) + } != 0 + { + return Vec::new(); + } + bytes.truncate(size); + bytes + .split(|byte| *byte == 0) + .filter(|arg| !arg.is_empty()) + .map(|arg| String::from_utf8_lossy(arg).into_owned()) + .collect() +} pub struct Command { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -73,11 +124,16 @@ impl Column for Command { .collect::(); cmd.pop(); cmd + } else if proc.pid < 0 { + // A thread has no command line of its own, so show the name + // the collector settled on - the thread's own when it has one, + // the owning process's otherwise - bracketed like its pid. + format!("[{}]", path.name) } else { - String::from("") + String::new() } } else { - String::from("") + String::new() }; let fmt_content = sanitize_control_chars(&fmt_content); let raw_content = fmt_content.clone(); @@ -113,18 +169,19 @@ impl Column for Command { #[cfg(target_os = "freebsd")] impl Column for Command { fn add(&mut self, proc: &ProcessInfo) { - let command = if proc.curr_proc.arg.is_empty() { - let comm = crate::util::ptr_to_cstr(proc.curr_proc.info.comm.as_ref()); + let args = get_process_args(proc.pid); + let command = if args.is_empty() { + let comm = crate::util::ptr_to_cstr(proc.curr_proc.ki_comm.as_ref()); if let Ok(comm) = comm { format!("[{}]", comm.to_string_lossy()) } else { - String::from("") + String::new() } } else { - let mut x = String::from(""); - for arg in &proc.curr_proc.arg { - x.push_str(&arg); - x.push_str(" "); + let mut x = String::new(); + for arg in &args { + x.push_str(arg); + x.push(' '); } x }; diff --git a/src/columns/context_sw.rs b/src/columns/context_sw.rs index 982de8fe2..b4117c2fe 100644 --- a/src/columns/context_sw.rs +++ b/src/columns/context_sw.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct ContextSw { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -53,7 +53,7 @@ impl Column for ContextSw { #[cfg(target_os = "macos")] impl Column for ContextSw { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_task.ptinfo.pti_csw as u64; + let raw_content = proc.curr_task.pti_csw as u64; let fmt_content = bytify(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); @@ -67,7 +67,7 @@ impl Column for ContextSw { impl Column for ContextSw { fn add(&mut self, proc: &ProcessInfo) { let raw_content = - (proc.curr_proc.info.rusage.nvcsw + proc.curr_proc.info.rusage.nivcsw) as u64; + (proc.curr_proc.ki_rusage.ru_nvcsw + proc.curr_proc.ki_rusage.ru_nivcsw) as u64; let fmt_content = bytify(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/cpu_time.rs b/src/columns/cpu_time.rs index 76587168c..aa9effb61 100644 --- a/src/columns/cpu_time.rs +++ b/src/columns/cpu_time.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct CpuTime { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -44,9 +44,8 @@ impl Column for CpuTime { #[cfg(target_os = "macos")] impl Column for CpuTime { fn add(&mut self, proc: &ProcessInfo) { - let time_sec = (proc.curr_task.ptinfo.pti_total_user - + proc.curr_task.ptinfo.pti_total_system) - / 1_000_000_000u64; + let time_sec = + (proc.curr_task.pti_total_user + proc.curr_task.pti_total_system) / 1_000_000_000u64; let fmt_content = util::parse_time(time_sec); let raw_content = time_sec; @@ -76,10 +75,10 @@ impl Column for CpuTime { #[cfg(target_os = "freebsd")] impl Column for CpuTime { fn add(&mut self, proc: &ProcessInfo) { - let time_sec = ((proc.curr_proc.info.rusage.utime.sec * 1_000_000i64 - + proc.curr_proc.info.rusage.utime.usec - + proc.curr_proc.info.rusage.stime.sec * 1_000_000i64 - + proc.curr_proc.info.rusage.stime.usec) + let time_sec = ((proc.curr_proc.ki_rusage.ru_utime.tv_sec * 1_000_000i64 + + proc.curr_proc.ki_rusage.ru_utime.tv_usec + + proc.curr_proc.ki_rusage.ru_stime.tv_sec * 1_000_000i64 + + proc.curr_proc.ki_rusage.ru_stime.tv_usec) / 1_000_000) as u64; let fmt_content = util::parse_time(time_sec); diff --git a/src/columns/docker.rs b/src/columns/docker.rs index acc7e968f..9655b5cd3 100644 --- a/src/columns/docker.rs +++ b/src/columns/docker.rs @@ -8,13 +8,13 @@ use tokio::runtime::Runtime; pub struct Docker { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, #[cfg(any(target_os = "linux", target_os = "android"))] containers: HashMap, #[cfg(target_os = "macos")] - containers: HashMap, + containers: HashMap, available: bool, } @@ -57,7 +57,7 @@ impl Docker { impl Docker { pub fn new(header: Option, path: &str) -> Self { let header = header.unwrap_or_else(|| String::from("Docker")); - let unit = String::from(""); + let unit = String::new(); let mut containers = HashMap::new(); let mut available = true; if let Ok(docker) = dockworker::Docker::connect_with_unix(path) { @@ -70,7 +70,7 @@ impl Docker { let name = String::from(&c.Names[0][1..]); if let Ok(processes) = rt.block_on(docker.processes(c.Id.as_str())) { for p in processes { - if let Ok(pid) = p.pid.parse::() { + if let Ok(pid) = p.pid.parse::() { containers.insert(pid, name.clone()); } } @@ -164,7 +164,7 @@ impl Column for Docker { let fmt_content = if let Some(name) = self.containers.get(&proc.pid) { name.to_string() } else { - String::from("") + String::new() }; let raw_content = fmt_content.clone(); diff --git a/src/columns/eip.rs b/src/columns/eip.rs index 6e029bb42..a3181abfc 100644 --- a/src/columns/eip.rs +++ b/src/columns/eip.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Eip { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/elapsed_time.rs b/src/columns/elapsed_time.rs index 606cdc1fc..1a4f34c27 100644 --- a/src/columns/elapsed_time.rs +++ b/src/columns/elapsed_time.rs @@ -2,7 +2,6 @@ use crate::process::ProcessInfo; use crate::{column_default, Column}; #[cfg(not(target_os = "windows"))] use chrono::offset::TimeZone; -#[cfg(any(target_os = "linux", target_os = "android"))] use chrono::DateTime; use chrono::{Duration, Local}; #[cfg(any(target_os = "linux", target_os = "android"))] @@ -16,8 +15,8 @@ static TICKS_PER_SECOND: Lazy = Lazy::new(procfs::ticks_per_second); pub struct ElapsedTime { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, #[cfg(any(target_os = "linux", target_os = "android"))] boot_time: DateTime, @@ -62,6 +61,15 @@ fn format_duration(duration: Duration) -> String { } } +fn elapsed_since(start_time: DateTime) -> (Duration, String) { + if start_time.timestamp() <= 0 { + (Duration::zero(), String::new()) + } else { + let elapsed = Local::now().signed_duration_since(start_time); + (elapsed, format_duration(elapsed)) + } +} + #[cfg(any(target_os = "linux", target_os = "android"))] impl Column for ElapsedTime { fn add(&mut self, proc: &ProcessInfo) { @@ -69,8 +77,7 @@ impl Column for ElapsedTime { let seconds_since_boot = starttime as f32 / *TICKS_PER_SECOND as f32; let start_time = self.boot_time + Duration::try_milliseconds((seconds_since_boot * 1000.0) as i64).unwrap_or_default(); - let raw_content = Local::now().signed_duration_since(start_time); - let fmt_content = format_duration(raw_content); + let (raw_content, fmt_content) = elapsed_since(start_time); self.fmt_contents.insert(proc.pid, fmt_content); self.raw_contents.insert(proc.pid, raw_content); @@ -83,10 +90,9 @@ impl Column for ElapsedTime { impl Column for ElapsedTime { fn add(&mut self, proc: &ProcessInfo) { let start_time = Local - .timestamp_opt(proc.curr_task.pbsd.pbi_start_tvsec as i64, 0) + .timestamp_opt(proc.curr_proc.kp_proc.p_starttime.tv_sec, 0) .unwrap(); - let raw_content = Local::now().signed_duration_since(start_time); - let fmt_content = format_duration(raw_content); + let (raw_content, fmt_content) = elapsed_since(start_time); self.fmt_contents.insert(proc.pid, fmt_content); self.raw_contents.insert(proc.pid, raw_content); @@ -98,8 +104,7 @@ impl Column for ElapsedTime { #[cfg(target_os = "windows")] impl Column for ElapsedTime { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = Local::now().signed_duration_since(proc.start_time); - let fmt_content = format_duration(raw_content); + let (raw_content, fmt_content) = elapsed_since(proc.start_time); self.fmt_contents.insert(proc.pid, fmt_content); self.raw_contents.insert(proc.pid, raw_content); @@ -112,10 +117,9 @@ impl Column for ElapsedTime { impl Column for ElapsedTime { fn add(&mut self, proc: &ProcessInfo) { let start_time = Local - .timestamp_opt(proc.curr_proc.info.start.sec as i64, 0) + .timestamp_opt(proc.curr_proc.ki_start.tv_sec, 0) .unwrap(); - let raw_content = Local::now().signed_duration_since(start_time); - let fmt_content = format_duration(raw_content); + let (raw_content, fmt_content) = elapsed_since(start_time); self.fmt_contents.insert(proc.pid, fmt_content); self.raw_contents.insert(proc.pid, raw_content); diff --git a/src/columns/empty.rs b/src/columns/empty.rs index 07d4f8c02..58fb80ef4 100644 --- a/src/columns/empty.rs +++ b/src/columns/empty.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Empty { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/env.rs b/src/columns/env.rs index 75ad1f4e0..cda144562 100644 --- a/src/columns/env.rs +++ b/src/columns/env.rs @@ -1,8 +1,12 @@ use crate::process::ProcessInfo; use crate::{Column, column_default}; +#[cfg(target_os = "freebsd")] +use libc::{CTL_KERN, KERN_PROC, KERN_PROC_ENV, c_void}; use std::cmp; use std::collections::HashMap; use std::path::PathBuf; +#[cfg(target_os = "freebsd")] +use std::ptr; #[cfg(target_os = "windows")] use windows_sys::Win32::Foundation::HANDLE; @@ -10,8 +14,8 @@ use windows_sys::Win32::Foundation::HANDLE; pub struct Env { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, #[allow(dead_code)] procfs: Option, @@ -32,6 +36,53 @@ impl Env { } } +#[cfg(target_os = "freebsd")] +pub(crate) fn get_process_env(pid: i64) -> Vec { + // A thread row carries its thread id negated, which is not a pid and + // names no process to ask about. `work_dir_of` guards the same way. + if pid <= 0 { + return Vec::new(); + } + + let mut mib = [CTL_KERN, KERN_PROC, KERN_PROC_ENV, pid as i32]; + let mut size = 0usize; + if unsafe { + libc::sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + ptr::null_mut(), + &mut size, + ptr::null_mut(), + 0, + ) + } != 0 + || size == 0 + { + return Vec::new(); + } + + let mut bytes = vec![0u8; size]; + if unsafe { + libc::sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + bytes.as_mut_ptr() as *mut c_void, + &mut size, + ptr::null_mut(), + 0, + ) + } != 0 + { + return Vec::new(); + } + bytes.truncate(size); + bytes + .split(|byte| *byte == 0) + .filter(|env| !env.is_empty()) + .map(|env| String::from_utf8_lossy(env).into_owned()) + .collect() +} + #[cfg(any(target_os = "linux", target_os = "android"))] impl Column for Env { fn add(&mut self, proc: &ProcessInfo) { @@ -60,8 +111,8 @@ impl Column for Env { impl Column for Env { fn add(&mut self, proc: &ProcessInfo) { let mut fmt_content = String::new(); - for env in &proc.curr_proc.env { - fmt_content.push_str(&format!("{} ", env.replace('\"', "\\\""))); + for env in get_process_env(proc.pid) { + fmt_content.push_str(&format!("{} ", env.replace('"', "\\\""))); } let raw_content = fmt_content.clone(); @@ -85,6 +136,41 @@ impl Column for Env { column_default!(String, false); } +/// The environment `KERN_PROCARGS2` reported with the process. +/// +/// The collector already asks for it to build the command line, so nothing is +/// queried here: `PathInfo::env` is the tail of the same `sysctl` buffer, the +/// NUL separated strings that follow the arguments. The kernel hands it out +/// for the processes of the user `procs` runs as, so the processes of other +/// users come through empty. +/// +/// A thread row is left blank: an environment belongs to a process, and the +/// row stands for a thread of one. +#[cfg(target_os = "macos")] +impl Column for Env { + fn add(&mut self, proc: &ProcessInfo) { + let mut fmt_content = String::new(); + if let Some(path) = &proc.curr_path { + for env in &path.env { + // The buffer holds raw `KEY=VALUE` strings; quote the value + // the way the Linux and Windows columns do, so that a value + // holding spaces still reads as one entry. + match env.split_once('=') { + Some((key, value)) => { + fmt_content.push_str(&format!("{}=\"{}\" ", key, value.replace('"', "\\\""))) + } + None => fmt_content.push_str(&format!("{} ", env.replace('"', "\\\""))), + } + } + } + + self.fmt_contents.insert(proc.pid, fmt_content.clone()); + self.raw_contents.insert(proc.pid, fmt_content); + } + + column_default!(String, false); +} + /// Reads the environment block of `pid` from its PEB. /// /// The PEB address comes from `NtQueryInformationProcess` with @@ -93,7 +179,7 @@ impl Column for Env { /// `PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ`, so protected /// processes (e.g. PPL) yield `None`. #[cfg(target_os = "windows")] -fn env_of(pid: i32) -> Option { +fn env_of(pid: i64) -> Option { use windows_sys::Win32::Foundation::{CloseHandle, FALSE, HANDLE}; use windows_sys::Win32::System::Threading::{ OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_READ, @@ -127,10 +213,11 @@ fn env_of(pid: i32) -> Option { #[cfg(target_os = "windows")] fn read_env(handle: HANDLE) -> Option { use crate::process::{ - process_peb_address, read_process_memory, PEB_PREFIX, + process_peb_address, read_process_memory, RTL_USER_PROCESS_PARAMETERS_PREFIX, }; use std::mem::{offset_of, size_of}; + use windows_sys::Win32::System::Threading::PEB; // The PEB address lives in the target's address space. let peb = process_peb_address(handle)?; @@ -146,7 +233,7 @@ fn read_env(handle: HANDLE) -> Option { peb_buf .as_ptr() .cast::() - .add(offset_of!(PEB_PREFIX, ProcessParameters)) + .add(offset_of!(PEB, ProcessParameters)) .cast::() .read() }; diff --git a/src/columns/esp.rs b/src/columns/esp.rs index 3877d1a88..80683b2d5 100644 --- a/src/columns/esp.rs +++ b/src/columns/esp.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Esp { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/file_name.rs b/src/columns/file_name.rs index 690df1181..171683ca8 100644 --- a/src/columns/file_name.rs +++ b/src/columns/file_name.rs @@ -1,13 +1,21 @@ use crate::process::ProcessInfo; use crate::{column_default, Column}; +#[cfg(target_os = "freebsd")] +use libc::{CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, c_void}; use std::cmp; use std::collections::HashMap; +#[cfg(target_os = "freebsd")] +use std::ffi::CStr; +#[cfg(target_os = "freebsd")] +use std::ptr; +#[cfg(target_os = "freebsd")] +use std::path::Path; pub struct FileName { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -25,6 +33,50 @@ impl FileName { } } +#[cfg(target_os = "freebsd")] +pub(crate) fn get_process_path(pid: i64) -> Option { + // A thread row carries its thread id negated, which is not a pid and + // names no process to ask about. `work_dir_of` guards the same way. + if pid <= 0 { + return None; + } + + let mut mib = [CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, pid as i32]; + let mut size = 0usize; + if unsafe { + libc::sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + ptr::null_mut(), + &mut size, + ptr::null_mut(), + 0, + ) + } != 0 + || size == 0 + { + return None; + } + + let mut bytes = vec![0u8; size]; + if unsafe { + libc::sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + bytes.as_mut_ptr() as *mut c_void, + &mut size, + ptr::null_mut(), + 0, + ) + } != 0 + { + return None; + } + CStr::from_bytes_until_nul(&bytes[..size]) + .ok() + .map(|path| path.to_string_lossy().into_owned()) +} + #[cfg(any(target_os = "linux", target_os = "android"))] impl Column for FileName { fn add(&mut self, proc: &ProcessInfo) { @@ -54,13 +106,26 @@ impl Column for FileName { #[cfg(target_os = "freebsd")] impl Column for FileName { fn add(&mut self, proc: &ProcessInfo) { - let comm = crate::util::ptr_to_cstr(proc.curr_proc.info.comm.as_ref()); - let comm = if let Ok(comm) = comm { - comm.to_string_lossy().into_owned() + let fmt_content = get_process_path(proc.pid) + .and_then(|path| Path::new(&path).file_name().map(|name| name.to_string_lossy().into_owned())) + .unwrap_or_default(); + let raw_content = fmt_content.clone(); + + self.fmt_contents.insert(proc.pid, fmt_content); + self.raw_contents.insert(proc.pid, raw_content); + } + + column_default!(String, false); +} + +#[cfg(target_os = "macos")] +impl Column for FileName { + fn add(&mut self, proc: &ProcessInfo) { + let fmt_content = if let Some(path) = &proc.curr_path { + path.name.clone() } else { - String::from("") + String::new() }; - let fmt_content = comm; let raw_content = fmt_content.clone(); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/gid.rs b/src/columns/gid.rs index 2dba3d0b8..eff4caee5 100644 --- a/src/columns/gid.rs +++ b/src/columns/gid.rs @@ -8,8 +8,8 @@ use std::collections::HashMap; pub struct Gid { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, #[allow(dead_code)] abbr_sid: bool, @@ -50,7 +50,12 @@ impl Column for Gid { #[cfg(target_os = "macos")] impl Column for Gid { fn add(&mut self, proc: &ProcessInfo) { - let gid = proc.curr_task.pbsd.pbi_gid; + // The effective gid is the first entry of the credential's group list + // (`kauth_cred_getgid`). `p_rgid` is the *real* gid and belongs to + // `GidReal`: the two do come apart - `WindowServer` runs with + // egid `_windowserver` and rgid `wheel` - so reading `p_rgid` here + // would just make this column a copy of `GidReal`. + let gid = proc.curr_proc.kp_eproc.e_ucred.cr_groups[0]; let fmt_content = format!("{}", gid); let raw_content = gid; @@ -86,7 +91,7 @@ impl Column for Gid { #[cfg(target_os = "freebsd")] impl Column for Gid { fn add(&mut self, proc: &ProcessInfo) { - let gid = proc.curr_proc.info.svgid; + let gid = proc.curr_proc.ki_svgid; let fmt_content = format!("{}", gid); let raw_content = gid; diff --git a/src/columns/gid_fs.rs b/src/columns/gid_fs.rs index 9f67375b7..c71752998 100644 --- a/src/columns/gid_fs.rs +++ b/src/columns/gid_fs.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct GidFs { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/gid_real.rs b/src/columns/gid_real.rs index a60d7d6a1..4e0b68ae1 100644 --- a/src/columns/gid_real.rs +++ b/src/columns/gid_real.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct GidReal { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -45,7 +45,7 @@ impl Column for GidReal { #[cfg(target_os = "macos")] impl Column for GidReal { fn add(&mut self, proc: &ProcessInfo) { - let gid = proc.curr_task.pbsd.pbi_rgid; + let gid = proc.curr_proc.kp_eproc.e_pcred.p_rgid; let fmt_content = format!("{}", gid); let raw_content = gid; @@ -59,7 +59,7 @@ impl Column for GidReal { #[cfg(target_os = "freebsd")] impl Column for GidReal { fn add(&mut self, proc: &ProcessInfo) { - let gid = proc.curr_proc.info.rgid; + let gid = proc.curr_proc.ki_rgid; let fmt_content = format!("{}", gid); let raw_content = gid; diff --git a/src/columns/gid_saved.rs b/src/columns/gid_saved.rs index b05b58cf4..8a8dc5143 100644 --- a/src/columns/gid_saved.rs +++ b/src/columns/gid_saved.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct GidSaved { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -45,7 +45,7 @@ impl Column for GidSaved { #[cfg(target_os = "macos")] impl Column for GidSaved { fn add(&mut self, proc: &ProcessInfo) { - let gid = proc.curr_task.pbsd.pbi_svgid; + let gid = proc.curr_proc.kp_eproc.e_pcred.p_svgid; let fmt_content = format!("{}", gid); let raw_content = gid; @@ -59,7 +59,7 @@ impl Column for GidSaved { #[cfg(target_os = "freebsd")] impl Column for GidSaved { fn add(&mut self, proc: &ProcessInfo) { - let gid = proc.curr_proc.info.svgid; + let gid = proc.curr_proc.ki_svgid; let fmt_content = format!("{}", gid); let raw_content = gid; diff --git a/src/columns/group.rs b/src/columns/group.rs index 05b334edf..629bced97 100644 --- a/src/columns/group.rs +++ b/src/columns/group.rs @@ -12,8 +12,8 @@ use uzers::Groups; pub struct Group { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, #[allow(dead_code)] abbr_sid: bool, @@ -59,7 +59,12 @@ impl Column for Group { #[cfg(target_os = "macos")] impl Column for Group { fn add(&mut self, proc: &ProcessInfo) { - let gid = proc.curr_task.pbsd.pbi_gid; + // The effective gid is the first entry of the credential's group list + // (`kauth_cred_getgid`). `p_rgid` is the *real* gid and belongs to + // `GroupReal`: the two do come apart - `WindowServer` runs with + // egid `_windowserver` and rgid `wheel` - so reading `p_rgid` here + // would just make this column a copy of `GroupReal`. + let gid = proc.curr_proc.kp_eproc.e_ucred.cr_groups[0]; let fmt_content = if let Some(group) = USERS_CACHE.with(|x| x.borrow_mut().get_group_by_gid(gid)) { format!("{}", group.name().to_string_lossy()) @@ -106,7 +111,7 @@ impl Column for Group { /// A thread row carries a thread id rather than a pid, so its lookup fails /// and it renders as empty - the same thing `Arch` and `WorkDir` do. #[cfg(target_os = "windows")] -pub fn groups_of(pid: i32) -> Option> { +pub fn groups_of(pid: i64) -> Option> { use std::mem::zeroed; use windows_sys::Win32::Foundation::{CloseHandle, FALSE, HANDLE}; use windows_sys::Win32::Security::{TOKEN_GROUPS, TOKEN_QUERY, TokenGroups}; @@ -183,7 +188,7 @@ pub fn primary_group(groups: &[SID_MAX]) -> Option<&SID_MAX> { #[cfg(target_os = "freebsd")] impl Column for Group { fn add(&mut self, proc: &ProcessInfo) { - let gid = proc.curr_proc.info.svgid; + let gid = proc.curr_proc.ki_svgid; let fmt_content = if let Some(group) = USERS_CACHE.with(|x| x.borrow_mut().get_group_by_gid(gid)) { format!("{}", group.name().to_string_lossy()) diff --git a/src/columns/group_fs.rs b/src/columns/group_fs.rs index c73f32595..d5c42339a 100644 --- a/src/columns/group_fs.rs +++ b/src/columns/group_fs.rs @@ -8,8 +8,8 @@ use uzers::Groups; pub struct GroupFs { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/group_real.rs b/src/columns/group_real.rs index ccd1d57a1..d32bf5b13 100644 --- a/src/columns/group_real.rs +++ b/src/columns/group_real.rs @@ -8,8 +8,8 @@ use uzers::Groups; pub struct GroupReal { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -52,7 +52,7 @@ impl Column for GroupReal { #[cfg(target_os = "macos")] impl Column for GroupReal { fn add(&mut self, proc: &ProcessInfo) { - let gid = proc.curr_task.pbsd.pbi_rgid; + let gid = proc.curr_proc.kp_eproc.e_pcred.p_rgid; let fmt_content = if let Some(group) = USERS_CACHE.with(|x| x.borrow_mut().get_group_by_gid(gid)) { format!("{}", group.name().to_string_lossy()) @@ -71,7 +71,7 @@ impl Column for GroupReal { #[cfg(target_os = "freebsd")] impl Column for GroupReal { fn add(&mut self, proc: &ProcessInfo) { - let gid = proc.curr_proc.info.rgid; + let gid = proc.curr_proc.ki_rgid; let fmt_content = if let Some(group) = USERS_CACHE.with(|x| x.borrow_mut().get_group_by_gid(gid)) { format!("{}", group.name().to_string_lossy()) diff --git a/src/columns/group_saved.rs b/src/columns/group_saved.rs index 01711cff5..b36e6dfb4 100644 --- a/src/columns/group_saved.rs +++ b/src/columns/group_saved.rs @@ -8,8 +8,8 @@ use uzers::Groups; pub struct GroupSaved { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -52,7 +52,7 @@ impl Column for GroupSaved { #[cfg(target_os = "macos")] impl Column for GroupSaved { fn add(&mut self, proc: &ProcessInfo) { - let gid = proc.curr_task.pbsd.pbi_svgid; + let gid = proc.curr_proc.kp_eproc.e_pcred.p_svgid; let fmt_content = if let Some(group) = USERS_CACHE.with(|x| x.borrow_mut().get_group_by_gid(gid)) { format!("{}", group.name().to_string_lossy()) @@ -71,7 +71,7 @@ impl Column for GroupSaved { #[cfg(target_os = "freebsd")] impl Column for GroupSaved { fn add(&mut self, proc: &ProcessInfo) { - let gid = proc.curr_proc.info.svgid; + let gid = proc.curr_proc.ki_svgid; let fmt_content = if let Some(group) = USERS_CACHE.with(|x| x.borrow_mut().get_group_by_gid(gid)) { format!("{}", group.name().to_string_lossy()) diff --git a/src/columns/involuntary_context_sw.rs b/src/columns/involuntary_context_sw.rs index 5e00be543..7e7588d52 100644 --- a/src/columns/involuntary_context_sw.rs +++ b/src/columns/involuntary_context_sw.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct InvoluntaryContextSw { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -50,7 +50,7 @@ impl Column for InvoluntaryContextSw { #[cfg(target_os = "freebsd")] impl Column for InvoluntaryContextSw { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_proc.info.rusage.nivcsw as u64; + let raw_content = proc.curr_proc.ki_rusage.ru_nivcsw as u64; let fmt_content = bytify(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/maj_flt.rs b/src/columns/maj_flt.rs index 7607408e2..6ba5333e9 100644 --- a/src/columns/maj_flt.rs +++ b/src/columns/maj_flt.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct MajFlt { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -41,7 +41,7 @@ impl Column for MajFlt { #[cfg(target_os = "macos")] impl Column for MajFlt { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_task.ptinfo.pti_pageins as u64; + let raw_content = proc.curr_task.pti_pageins as u64; let fmt_content = format!("{raw_content}"); self.fmt_contents.insert(proc.pid, fmt_content); @@ -67,7 +67,7 @@ impl Column for MajFlt { #[cfg(target_os = "freebsd")] impl Column for MajFlt { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_proc.info.rusage.majflt as u64; + let raw_content = proc.curr_proc.ki_rusage.ru_majflt as u64; let fmt_content = format!("{raw_content}"); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/min_flt.rs b/src/columns/min_flt.rs index a632788a7..03d7cc769 100644 --- a/src/columns/min_flt.rs +++ b/src/columns/min_flt.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct MinFlt { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -41,8 +41,7 @@ impl Column for MinFlt { #[cfg(target_os = "macos")] impl Column for MinFlt { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = - (proc.curr_task.ptinfo.pti_faults - proc.curr_task.ptinfo.pti_pageins) as u64; + let raw_content = (proc.curr_task.pti_faults - proc.curr_task.pti_pageins) as u64; let fmt_content = format!("{}", raw_content); self.fmt_contents.insert(proc.pid, fmt_content); @@ -55,7 +54,7 @@ impl Column for MinFlt { #[cfg(target_os = "freebsd")] impl Column for MinFlt { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_proc.info.rusage.minflt as u64; + let raw_content = proc.curr_proc.ki_rusage.ru_minflt as u64; let fmt_content = format!("{raw_content}"); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/multi_slot.rs b/src/columns/multi_slot.rs index 4461f0973..7f6741d8e 100644 --- a/src/columns/multi_slot.rs +++ b/src/columns/multi_slot.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct MultiSlot { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/nice.rs b/src/columns/nice.rs index 8352da0d3..8c918953f 100644 --- a/src/columns/nice.rs +++ b/src/columns/nice.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Nice { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -41,7 +41,7 @@ impl Column for Nice { #[cfg(target_os = "macos")] impl Column for Nice { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_task.pbsd.pbi_nice as i64; + let raw_content = proc.curr_proc.kp_proc.p_nice as i64; let fmt_content = format!("{}", raw_content); self.fmt_contents.insert(proc.pid, fmt_content); @@ -54,7 +54,7 @@ impl Column for Nice { #[cfg(target_os = "freebsd")] impl Column for Nice { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_proc.info.nice as i64; + let raw_content = proc.curr_proc.ki_nice as i64; let fmt_content = format!("{raw_content}"); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/os_freebsd.rs b/src/columns/os_freebsd.rs index 56e03723a..9f470f2de 100644 --- a/src/columns/os_freebsd.rs +++ b/src/columns/os_freebsd.rs @@ -49,6 +49,7 @@ pub mod vm_rss; pub mod vm_size; pub mod vm_stack; pub mod wchan; +pub mod work_dir; pub mod write_bytes; pub use self::command::Command; @@ -102,6 +103,7 @@ pub use self::vm_rss::VmRss; pub use self::vm_size::VmSize; pub use self::vm_stack::VmStack; pub use self::wchan::Wchan; +pub use self::work_dir::WorkDir; pub use self::write_bytes::WriteBytes; use crate::column::Column; @@ -167,6 +169,7 @@ pub enum ConfigColumnKind { VmSize, VmStack, Wchan, + WorkDir, WriteBytes, } @@ -235,6 +238,7 @@ pub fn gen_column( ConfigColumnKind::VmSize => Box::new(VmSize::new(header)), ConfigColumnKind::VmStack => Box::new(VmStack::new(header)), ConfigColumnKind::Wchan => Box::new(Wchan::new(header)), + ConfigColumnKind::WorkDir => Box::new(WorkDir::new(header, procfs)), ConfigColumnKind::WriteBytes => Box::new(WriteBytes::new(header)), } } @@ -350,6 +354,10 @@ pub static KIND_LIST: Lazy Box::new(Empty::new()), ConfigColumnKind::ElapsedTime => Box::new(ElapsedTime::new(header)), ConfigColumnKind::Empty => Box::new(Empty::new()), + ConfigColumnKind::Env => Box::new(Env::new(header, _procfs)), + ConfigColumnKind::FileName => Box::new(FileName::new(header)), ConfigColumnKind::Gid => Box::new(Gid::new(header, abbr_sid)), ConfigColumnKind::GidReal => Box::new(GidReal::new(header)), ConfigColumnKind::GidSaved => Box::new(GidSaved::new(header)), @@ -218,6 +229,7 @@ pub fn gen_column( ConfigColumnKind::VmRss => Box::new(VmRss::new(header)), ConfigColumnKind::VmSize => Box::new(VmSize::new(header)), ConfigColumnKind::VmTotal => Box::new(VmTotal::new(header)), + ConfigColumnKind::WorkDir => Box::new(WorkDir::new(header, _procfs)), ConfigColumnKind::WriteBytes => Box::new(WriteBytes::new(header)), } } @@ -250,6 +262,8 @@ pub static KIND_LIST: Lazy, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -29,9 +29,13 @@ impl Pgid { impl Column for Pgid { fn add(&mut self, proc: &ProcessInfo) { let raw_content = proc.curr_proc.stat().pgrp; - let fmt_content = match proc.curr_proc { - crate::process::ProcessTask::Process { .. } => format!("{raw_content}"), - _ => format!("[{raw_content}]"), + // A thread row is bracketed the way its pid is: the value comes from + // the shared `stat`, and the bracket is what keeps it from reading + // like the process group of a process. + let fmt_content = if proc.pid < 0 { + format!("[{raw_content}]") + } else { + format!("{raw_content}") }; self.fmt_contents.insert(proc.pid, fmt_content); @@ -44,7 +48,7 @@ impl Column for Pgid { #[cfg(target_os = "macos")] impl Column for Pgid { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_task.pbsd.pbi_pgid as i32; + let raw_content = proc.curr_proc.kp_eproc.e_pgid; let fmt_content = format!("{}", raw_content); self.fmt_contents.insert(proc.pid, fmt_content); @@ -57,7 +61,7 @@ impl Column for Pgid { #[cfg(target_os = "freebsd")] impl Column for Pgid { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_proc.info.pgid as i32; + let raw_content = proc.curr_proc.ki_pgid; let fmt_content = format!("{}", raw_content); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/pid.rs b/src/columns/pid.rs index 512d64ffd..892987f1c 100644 --- a/src/columns/pid.rs +++ b/src/columns/pid.rs @@ -1,13 +1,14 @@ -use crate::process::ProcessInfo; -use crate::{column_default, Column}; +use crate::config::ConfigSortOrder; +use crate::process::{ProcessInfo, row_sort_key}; +use crate::{Column, column_default_without_sorted_pid}; use std::cmp; use std::collections::HashMap; pub struct Pid { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -25,31 +26,38 @@ impl Pid { } } -#[cfg(any(target_os = "linux", target_os = "android"))] impl Column for Pid { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.pid; - let fmt_content = match proc.curr_proc { - crate::process::ProcessTask::Process { .. } => format!("{raw_content}"), - _ => format!("[{raw_content}]"), + // The id the row stands for, not the key that carries it: a thread row + // is keyed by its negated thread id, and the value behind this column - + // what `--json` reports and what a regex search reads - has to be the + // thread id itself rather than the internal negation of it. + // + // Negating the key cannot overflow: it is either a process id, which + // is not negative, or a `thread_key`, which is never `i64::MIN`. + let raw_content = if proc.pid >= 0 { proc.pid } else { -proc.pid }; + // Printed bracketed the way `ps` prints a kernel thread, which is what + // keeps the id of a thread from reading like the id of a process. + let fmt_content = if proc.pid >= 0 { + format!("{raw_content}") + } else { + format!("[{raw_content}]") }; self.fmt_contents.insert(proc.pid, fmt_content); self.raw_contents.insert(proc.pid, raw_content); } - column_default!(i32, true); -} - -#[cfg(not(any(target_os = "linux", target_os = "android")))] -impl Column for Pid { - fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.pid; - let fmt_content = format!("{raw_content}"); - - self.fmt_contents.insert(proc.pid, fmt_content); - self.raw_contents.insert(proc.pid, raw_content); + /// Rows are ordered by the id they stand for rather than by the key that + /// carries it - see [`crate::process::row_sort_key`]. + fn sorted_pid(&self, order: &ConfigSortOrder) -> Vec { + let mut contents: Vec<(&i64, &i64)> = self.raw_contents.iter().collect(); + contents.sort_by_key(|&(pid, _)| row_sort_key(*pid)); + if matches!(*order, ConfigSortOrder::Descending) { + contents.reverse() + } + contents.iter().map(|(pid, _)| **pid).collect() } - column_default!(i32, true); + column_default_without_sorted_pid!(true); } diff --git a/src/columns/policy.rs b/src/columns/policy.rs index 9cd813e59..b45e0ef47 100644 --- a/src/columns/policy.rs +++ b/src/columns/policy.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Policy { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -55,7 +55,7 @@ impl Column for Policy { Some(libc::SCHED_BATCH) => String::from("B"), Some(libc::SCHED_IDLE) => String::from("IDL"), Some(libc::SCHED_DEADLINE) => String::from("D"), - _ => String::from(""), + _ => String::new(), }; let raw_content = fmt_content.clone(); @@ -69,11 +69,11 @@ impl Column for Policy { #[cfg(target_os = "macos")] impl Column for Policy { fn add(&mut self, proc: &ProcessInfo) { - let fmt_content = match proc.curr_task.ptinfo.pti_policy { + let fmt_content = match proc.curr_task.pti_policy { 1 => String::from("TS"), 2 => String::from("RR"), 4 => String::from("FF"), - _ => String::from(""), + _ => String::new(), }; let raw_content = fmt_content.clone(); diff --git a/src/columns/ppid.rs b/src/columns/ppid.rs index 26bbcda51..497a85a8a 100644 --- a/src/columns/ppid.rs +++ b/src/columns/ppid.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Ppid { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -34,5 +34,5 @@ impl Column for Ppid { self.raw_contents.insert(proc.pid, raw_content); } - column_default!(i32, true); + column_default!(i64, true); } diff --git a/src/columns/priority.rs b/src/columns/priority.rs index 6caddf5af..af6dc1ca3 100644 --- a/src/columns/priority.rs +++ b/src/columns/priority.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Priority { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -41,7 +41,7 @@ impl Column for Priority { #[cfg(target_os = "macos")] impl Column for Priority { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_task.ptinfo.pti_priority as i64; + let raw_content = proc.curr_task.pti_priority as i64; let fmt_content = format!("{}", raw_content); self.fmt_contents.insert(proc.pid, fmt_content); @@ -81,7 +81,7 @@ impl Column for Priority { } #[cfg(target_os = "windows")] -fn priority_class_of(pid: i32) -> Option { +fn priority_class_of(pid: i64) -> Option { use windows_sys::Win32::Foundation::{CloseHandle, FALSE, HANDLE}; use windows_sys::Win32::System::Threading::{ GetPriorityClass, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, @@ -137,11 +137,11 @@ fn priority_class_from_base(base: i32) -> u32 { // Base priorities: Idle=4, BelowNormal=6, Normal=8, AboveNormal=10, // High=13, Realtime=24. Pick the class whose base value is closest. match base { - b if b <= ((4 + 6) / 2) as i32 => IDLE_PRIORITY_CLASS, - b if b <= ((6 + 8) / 2) as i32 => BELOW_NORMAL_PRIORITY_CLASS, - b if b <= ((8 + 10) / 2) as i32 => NORMAL_PRIORITY_CLASS, - b if b <= ((10 + 13) / 2) as i32 => ABOVE_NORMAL_PRIORITY_CLASS, - b if b <= ((13 + 24) / 2) as i32 => HIGH_PRIORITY_CLASS, + b if b <= ((4 + 6) / 2) => IDLE_PRIORITY_CLASS, + b if b <= ((6 + 8) / 2) => BELOW_NORMAL_PRIORITY_CLASS, + b if b <= ((8 + 10) / 2) => NORMAL_PRIORITY_CLASS, + b if b <= ((10 + 13) / 2) => ABOVE_NORMAL_PRIORITY_CLASS, + b if b <= ((13 + 24) / 2) => HIGH_PRIORITY_CLASS, _ => REALTIME_PRIORITY_CLASS, } } @@ -149,7 +149,7 @@ fn priority_class_from_base(base: i32) -> u32 { #[cfg(target_os = "freebsd")] impl Column for Priority { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_proc.info.pri.level as i64 - 100; + let raw_content = proc.curr_proc.ki_pri.pri_level as i64 - 100; let fmt_content = format!("{raw_content}"); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/processor.rs b/src/columns/processor.rs index b1e490b72..cdfc00cba 100644 --- a/src/columns/processor.rs +++ b/src/columns/processor.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Processor { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -45,7 +45,7 @@ impl Column for Processor { #[cfg(target_os = "freebsd")] impl Column for Processor { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_proc.info.lastcpu; + let raw_content = proc.curr_proc.ki_lastcpu; let fmt_content = format!("{raw_content}"); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/read_bytes.rs b/src/columns/read_bytes.rs index 2f6450a46..72a01d1fa 100644 --- a/src/columns/read_bytes.rs +++ b/src/columns/read_bytes.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct ReadBytes { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -49,16 +49,15 @@ impl Column for ReadBytes { #[cfg(target_os = "macos")] impl Column for ReadBytes { fn add(&mut self, proc: &ProcessInfo) { - let (fmt_content, raw_content) = if proc.curr_res.is_some() && proc.prev_res.is_some() { - let interval_ms = proc.interval.as_secs() * 1000 + u64::from(proc.interval.subsec_millis()); - let io = (proc.curr_res.as_ref().unwrap().ri_diskio_bytesread - - proc.prev_res.as_ref().unwrap().ri_diskio_bytesread) - * 1000 - / interval_ms; - (bytify(io), io) - } else { - (String::from(""), 0) - }; + let (fmt_content, raw_content) = + if let (Some(curr), Some(prev)) = (proc.curr_res.as_ref(), proc.prev_res.as_ref()) { + let interval_ms = + proc.interval.as_secs() * 1000 + u64::from(proc.interval.subsec_millis()); + let io = (curr.ri_diskio_bytesread - prev.ri_diskio_bytesread) * 1000 / interval_ms; + (bytify(io), io) + } else { + (String::new(), 0) + }; self.fmt_contents.insert(proc.pid, fmt_content); self.raw_contents.insert(proc.pid, raw_content); @@ -89,8 +88,8 @@ impl Column for ReadBytes { // io block size: 128KB let block_size = 128 * 1024; let interval_ms = proc.interval.as_secs() * 1000 + u64::from(proc.interval.subsec_millis()); - let io = (proc.curr_proc.info.rusage.inblock as u64 - - proc.prev_proc.info.rusage.inblock as u64) + let io = (proc.curr_proc.ki_rusage.ru_inblock as u64 + - proc.prev_proc.ki_rusage.ru_inblock as u64) * block_size * 1000 / interval_ms; diff --git a/src/columns/recv_bytes.rs b/src/columns/recv_bytes.rs index bd7a81c5d..7f5daf503 100644 --- a/src/columns/recv_bytes.rs +++ b/src/columns/recv_bytes.rs @@ -14,8 +14,8 @@ use std::collections::HashMap; pub struct RecvBytes { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/rt_priority.rs b/src/columns/rt_priority.rs index 825b0ef1c..ef80dacca 100644 --- a/src/columns/rt_priority.rs +++ b/src/columns/rt_priority.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct RtPriority { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/sec_context.rs b/src/columns/sec_context.rs index 3f399e9f6..4f0059145 100644 --- a/src/columns/sec_context.rs +++ b/src/columns/sec_context.rs @@ -9,8 +9,8 @@ use std::path::PathBuf; pub struct SecContext { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, procfs: Option, } @@ -39,10 +39,10 @@ impl Column for SecContext { let _ = file.read_to_string(&mut ret); ret.trim_end_matches('\x00').to_string() } else { - String::from("") + String::new() } } else { - String::from("") + String::new() }; let raw_content = fmt_content.clone(); diff --git a/src/columns/send_bytes.rs b/src/columns/send_bytes.rs index 95fcfc1b0..5a7e08834 100644 --- a/src/columns/send_bytes.rs +++ b/src/columns/send_bytes.rs @@ -14,8 +14,8 @@ use std::collections::HashMap; pub struct SendBytes { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/separator.rs b/src/columns/separator.rs index a98f0988b..81bd1f8d0 100644 --- a/src/columns/separator.rs +++ b/src/columns/separator.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Separator { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, separator: String, } diff --git a/src/columns/session.rs b/src/columns/session.rs index 9380e1c2b..b2391658b 100644 --- a/src/columns/session.rs +++ b/src/columns/session.rs @@ -8,8 +8,8 @@ use std::collections::HashMap; pub struct Session { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -31,9 +31,13 @@ impl Session { impl Column for Session { fn add(&mut self, proc: &ProcessInfo) { let raw_content = proc.curr_proc.stat().session; - let fmt_content = match proc.curr_proc { - crate::process::ProcessTask::Process { .. } => format!("{raw_content}"), - _ => format!("[{raw_content}]"), + // A thread row is bracketed the way its pid is: the value comes from + // the shared `stat`, and the bracket is what keeps it from reading + // like the session of a process. + let fmt_content = if proc.pid < 0 { + format!("[{raw_content}]") + } else { + format!("{raw_content}") }; self.fmt_contents.insert(proc.pid, fmt_content); @@ -46,10 +50,17 @@ impl Column for Session { #[cfg(target_os = "macos")] impl Column for Session { fn add(&mut self, proc: &ProcessInfo) { - let sid = unistd::getsid(Some(Pid::from_raw(proc.pid))) - .map(|x| x.as_raw()) - .unwrap_or(0); - let raw_content = sid; + // A session belongs to a process, and a thread row is keyed by its + // negated thread id, which is 64 bit wide: handing it to `getsid` + // would truncate it and could report the session of an unrelated + // process. So the row is left at 0. + let raw_content = if proc.pid < 0{ + 0 + } else { + unistd::getsid(Some(Pid::from_raw(proc.pid as i32))) + .map(|x| x.as_raw()) + .unwrap_or(0) + }; let fmt_content = format!("{}", raw_content); self.fmt_contents.insert(proc.pid, fmt_content); @@ -75,7 +86,7 @@ impl Column for Session { #[cfg(target_os = "freebsd")] impl Column for Session { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_proc.info.sid; + let raw_content = proc.curr_proc.ki_sid; let fmt_content = format!("{}", raw_content); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/shd_pnd.rs b/src/columns/shd_pnd.rs index f2c7f040d..762bde85e 100644 --- a/src/columns/shd_pnd.rs +++ b/src/columns/shd_pnd.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct ShdPnd { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -45,7 +45,7 @@ impl Column for ShdPnd { #[cfg(target_os = "freebsd")] impl Column for ShdPnd { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_proc.info.siglist.0[0] as u64; + let raw_content = crate::util::sigset_words(&proc.curr_proc.ki_siglist)[0] as u64; let fmt_content = format!("{raw_content:016x}"); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/sig_blk.rs b/src/columns/sig_blk.rs index d2ae34439..c7bcaf70c 100644 --- a/src/columns/sig_blk.rs +++ b/src/columns/sig_blk.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct SigBlk { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -45,7 +45,7 @@ impl Column for SigBlk { #[cfg(target_os = "freebsd")] impl Column for SigBlk { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_proc.info.sigmask.0[0] as u64; + let raw_content = crate::util::sigset_words(&proc.curr_proc.ki_sigmask)[0] as u64; let fmt_content = format!("{raw_content:016x}"); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/sig_cgt.rs b/src/columns/sig_cgt.rs index e50b55379..5c42b5f25 100644 --- a/src/columns/sig_cgt.rs +++ b/src/columns/sig_cgt.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct SigCgt { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -45,7 +45,7 @@ impl Column for SigCgt { #[cfg(target_os = "freebsd")] impl Column for SigCgt { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_proc.info.sigcatch.0[0] as u64; + let raw_content = crate::util::sigset_words(&proc.curr_proc.ki_sigcatch)[0] as u64; let fmt_content = format!("{raw_content:016x}"); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/sig_ign.rs b/src/columns/sig_ign.rs index 19b179e9d..4cca0124e 100644 --- a/src/columns/sig_ign.rs +++ b/src/columns/sig_ign.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct SigIgn { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -45,7 +45,7 @@ impl Column for SigIgn { #[cfg(target_os = "freebsd")] impl Column for SigIgn { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_proc.info.sigignore.0[0] as u64; + let raw_content = crate::util::sigset_words(&proc.curr_proc.ki_sigignore)[0] as u64; let fmt_content = format!("{raw_content:016x}"); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/sig_pnd.rs b/src/columns/sig_pnd.rs index bfd49855d..96d9040df 100644 --- a/src/columns/sig_pnd.rs +++ b/src/columns/sig_pnd.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct SigPnd { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/slot.rs b/src/columns/slot.rs index 992959e65..566fbc221 100644 --- a/src/columns/slot.rs +++ b/src/columns/slot.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Slot { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/ssb.rs b/src/columns/ssb.rs index c95277fac..d9b7030d7 100644 --- a/src/columns/ssb.rs +++ b/src/columns/ssb.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Ssb { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/start_time.rs b/src/columns/start_time.rs index b58bb92ba..5e7926035 100644 --- a/src/columns/start_time.rs +++ b/src/columns/start_time.rs @@ -16,8 +16,8 @@ static TICKS_PER_SECOND: Lazy = Lazy::new(procfs::ticks_per_second); pub struct StartTime { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap>, + fmt_contents: HashMap, + raw_contents: HashMap>, width: usize, #[cfg(any(target_os = "linux", target_os = "android"))] boot_time: DateTime, @@ -39,6 +39,14 @@ impl StartTime { } } +fn format_start_time(start_time: DateTime) -> String { + if start_time.timestamp() <= 0 { + String::new() + } else { + format!("{}", start_time.format("%Y/%m/%d %H:%M")) + } +} + #[cfg(any(target_os = "linux", target_os = "android"))] impl Column for StartTime { fn add(&mut self, proc: &ProcessInfo) { @@ -46,7 +54,7 @@ impl Column for StartTime { let seconds_since_boot = starttime as f32 / *TICKS_PER_SECOND as f32; let raw_content = self.boot_time + Duration::try_milliseconds((seconds_since_boot * 1000.0) as i64).unwrap_or_default(); - let fmt_content = format!("{}", raw_content.format("%Y/%m/%d %H:%M")); + let fmt_content = format_start_time(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); self.raw_contents.insert(proc.pid, raw_content); @@ -59,10 +67,10 @@ impl Column for StartTime { impl Column for StartTime { fn add(&mut self, proc: &ProcessInfo) { let start_time = Local - .timestamp_opt(proc.curr_task.pbsd.pbi_start_tvsec as i64, 0) + .timestamp_opt(proc.curr_proc.kp_proc.p_starttime.tv_sec, 0) .unwrap(); let raw_content = start_time; - let fmt_content = format!("{}", start_time.format("%Y/%m/%d %H:%M")); + let fmt_content = format_start_time(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); self.raw_contents.insert(proc.pid, raw_content); @@ -75,7 +83,7 @@ impl Column for StartTime { impl Column for StartTime { fn add(&mut self, proc: &ProcessInfo) { let raw_content = proc.start_time; - let fmt_content = format!("{}", proc.start_time.format("%Y/%m/%d %H:%M")); + let fmt_content = format_start_time(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); self.raw_contents.insert(proc.pid, raw_content); @@ -88,10 +96,10 @@ impl Column for StartTime { impl Column for StartTime { fn add(&mut self, proc: &ProcessInfo) { let start_time = Local - .timestamp_opt(proc.curr_proc.info.start.sec as i64, 0) + .timestamp_opt(proc.curr_proc.ki_start.tv_sec, 0) .unwrap(); let raw_content = start_time; - let fmt_content = format!("{}", start_time.format("%Y/%m/%d %H:%M")); + let fmt_content = format_start_time(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); self.raw_contents.insert(proc.pid, raw_content); diff --git a/src/columns/state.rs b/src/columns/state.rs index 2aa893c38..2d8d02f3d 100644 --- a/src/columns/state.rs +++ b/src/columns/state.rs @@ -9,8 +9,8 @@ use crate::process::{thread_state, wait_reason, ThreadState}; pub struct State { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -44,25 +44,7 @@ impl Column for State { #[cfg(target_os = "macos")] impl Column for State { fn add(&mut self, proc: &ProcessInfo) { - let mut state = 7; - for t in &proc.curr_threads { - let s = match t.pth_run_state { - 1 => 1, // TH_STATE_RUNNING - 2 => 5, // TH_STATE_STOPPED - 3 => { - if t.pth_sleep_time > 20 { - 4 - } else { - 3 - } - } // TH_STATE_WAITING - 4 => 2, // TH_STATE_UNINTERRUPTIBLE - 5 => 6, // TH_STATE_HALTED - _ => 7, - }; - state = cmp::min(s, state); - } - let state = match state { + let state = match proc.state { 0 => "", 1 => "R", 2 => "U", @@ -70,6 +52,7 @@ impl Column for State { 4 => "I", 5 => "T", 6 => "H", + 8 => "Z", _ => "?", }; let fmt_content = state.to_string(); @@ -85,17 +68,17 @@ impl Column for State { #[cfg(target_os = "freebsd")] impl Column for State { fn add(&mut self, proc: &ProcessInfo) { - let info = &proc.curr_proc.info; - let flag = info.flag; - let tdflags = info.tdflags; - let cr_flags = info.cr_flags; - let kiflag = info.kiflag; + let info = &proc.curr_proc; + let flag = info.ki_flag; + let tdflags = info.ki_tdflags; + let cr_flags = info.ki_cr_flags; + let kiflag = info.ki_kiflag; - let mut state = match info.stat { + let mut state = match info.ki_stat { libc::SSTOP => "T", libc::SSLEEP => { if (tdflags & libc::TDF_SINTR as i64) != 0 { - if info.slptime >= 20 { + if info.ki_slptime >= 20 { "I" } else { "S" @@ -112,37 +95,41 @@ impl Column for State { } .to_string(); if (flag & libc::P_INMEM as i64) == 0 { - state.push_str("W"); + state.push('W'); } - if info.nice < libc::NZERO as i8 || info.pri.class == bsd_kvm_sys::PRI_REALTIME as u8 { - state.push_str("<"); + if info.ki_nice < libc::NZERO as i8 + || info.ki_pri.pri_class == crate::process::PRI_REALTIME + { + state.push('<'); } - if info.nice > libc::NZERO as i8 || info.pri.class == bsd_kvm_sys::PRI_IDLE as u8 { - state.push_str("N"); + if info.ki_nice > libc::NZERO as i8 || info.ki_pri.pri_class == crate::process::PRI_IDLE { + state.push('N'); } if (flag & libc::P_TRACED as i64) != 0 { - state.push_str("X"); + state.push('X'); } - if (flag & libc::P_WEXIT as i64) != 0 && info.stat != libc::SZOMB as std::os::raw::c_char { - state.push_str("E"); + if (flag & libc::P_WEXIT as i64) != 0 + && info.ki_stat != libc::SZOMB as std::os::raw::c_char + { + state.push('E'); } if (flag & libc::P_PPWAIT as i64) != 0 { - state.push_str("V"); + state.push('V'); } - if (flag & libc::P_SYSTEM as i64) != 0 || info.lock > 0 { - state.push_str("L"); + if (flag & libc::P_SYSTEM as i64) != 0 || info.ki_lock > 0 { + state.push('L'); } if (cr_flags & libc::KI_CRF_CAPABILITY_MODE as u32) != 0 { - state.push_str("C"); + state.push('C'); } if (kiflag & libc::KI_SLEADER as i64) != 0 { - state.push_str("s"); + state.push('s'); } - if (flag & libc::P_CONTROLT as i64) != 0 && info.pgid == info.tpgid { - state.push_str("+"); + if (flag & libc::P_CONTROLT as i64) != 0 && info.ki_pgid == info.ki_tpgid { + state.push('+'); } if (flag & libc::P_JAILED as i64) != 0 { - state.push_str("J"); + state.push('J'); } let fmt_content = state; let raw_content = fmt_content.clone(); diff --git a/src/columns/tcp_port.rs b/src/columns/tcp_port.rs index 47a8880e5..f0590a558 100644 --- a/src/columns/tcp_port.rs +++ b/src/columns/tcp_port.rs @@ -22,8 +22,8 @@ use windows_sys::Win32::Networking::WinSock::{ntohl, ntohs}; pub struct TcpPort { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, #[cfg(any(target_os = "linux", target_os = "android", target_os = "windows"))] tcp_entry: Vec, @@ -87,7 +87,7 @@ impl Column for TcpPort { self.raw_contents.insert(proc.pid, raw_content); } - fn find_exact(&self, pid: i32, keyword: &str, _content_to_lowercase: bool) -> bool { + fn find_exact(&self, pid: i64, keyword: &str, _content_to_lowercase: bool) -> bool { if let Some(content) = self.fmt_contents.get(&pid) { let content = content.replace(['[', ']'], ""); let content = content.split(','); @@ -135,7 +135,7 @@ impl Column for TcpPort { self.raw_contents.insert(proc.pid, raw_content); } - fn find_exact(&self, pid: i32, keyword: &str, _content_to_lowercase: bool) -> bool { + fn find_exact(&self, pid: i64, keyword: &str, _content_to_lowercase: bool) -> bool { if let Some(content) = self.fmt_contents.get(&pid) { let content = content.replace(['[', ']'], ""); let content = content.split(','); @@ -181,7 +181,7 @@ impl Column for TcpPort { self.raw_contents.insert(proc.pid, raw_content); } - fn find_exact(&self, pid: i32, keyword: &str, _content_to_lowercase: bool) -> bool { + fn find_exact(&self, pid: i64, keyword: &str, _content_to_lowercase: bool) -> bool { if let Some(content) = self.fmt_contents.get(&pid) { let content = content.replace(['[', ']'], ""); let content = content.split(','); diff --git a/src/columns/threads.rs b/src/columns/threads.rs index 15965124e..5960a09f0 100644 --- a/src/columns/threads.rs +++ b/src/columns/threads.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Threads { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -41,7 +41,7 @@ impl Column for Threads { #[cfg(target_os = "macos")] impl Column for Threads { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_task.ptinfo.pti_threadnum as i64; + let raw_content = proc.curr_task.pti_threadnum as i64; let fmt_content = format!("{}", raw_content); self.fmt_contents.insert(proc.pid, fmt_content); @@ -67,7 +67,7 @@ impl Column for Threads { #[cfg(target_os = "freebsd")] impl Column for Threads { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_proc.info.numthreads as i64; + let raw_content = proc.curr_proc.ki_numthreads as i64; let fmt_content = format!("{}", raw_content); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/tree.rs b/src/columns/tree.rs index 2368503cc..5aa0f6e4a 100644 --- a/src/columns/tree.rs +++ b/src/columns/tree.rs @@ -1,5 +1,5 @@ use crate::Column; -use crate::process::ProcessInfo; +use crate::process::{ProcessInfo, row_sort_key}; use std::cmp; use std::collections::HashMap; @@ -7,8 +7,8 @@ pub struct Tree { header: String, unit: String, width: usize, - tree: HashMap>, - rev_tree: HashMap, + tree: HashMap>, + rev_tree: HashMap, symbols: [String; 5], } @@ -31,7 +31,10 @@ impl Column for Tree { fn add(&mut self, proc: &ProcessInfo) { if let Some(node) = self.tree.get_mut(&proc.ppid) { node.push(proc.pid); - node.sort_unstable(); + // Siblings are ordered the way the Pid column orders rows, so a + // thread sits next to the process whose id it was handed instead + // of before every process. + node.sort_unstable_by_key(|pid| row_sort_key(*pid)); } else { self.tree.insert(proc.ppid, vec![proc.pid]); } @@ -49,14 +52,14 @@ impl Column for Tree { fn display_content( &self, - pid: i32, + pid: i64, align: &crate::config::ConfigColumnAlign, ) -> Option { fn gen_root( - tree: &HashMap>, - rev_tree: &HashMap, + tree: &HashMap>, + rev_tree: &HashMap, symbols: &[String; 5], - pid: i32, + pid: i64, mut string: String, ) -> String { if let Some(ppid) = rev_tree.get(&pid) { @@ -64,7 +67,10 @@ impl Column for Tree { string } else if let Some(pppid) = rev_tree.get(ppid) { let brother = tree.get(pppid).unwrap(); - let is_last = brother.binary_search(ppid).unwrap() == brother.len() - 1; + let is_last = brother + .binary_search_by_key(&row_sort_key(*ppid), |pid| row_sort_key(*pid)) + .unwrap() + == brother.len() - 1; if is_last { string.push(' '); @@ -91,7 +97,10 @@ impl Column for Tree { let root: String = root.chars().rev().collect(); let brother = &self.tree[ppid]; - let is_last = brother.binary_search(&pid).unwrap() == brother.len() - 1; + let is_last = brother + .binary_search_by_key(&row_sort_key(pid), |pid| row_sort_key(*pid)) + .unwrap() + == brother.len() - 1; let has_child = self.tree.contains_key(&pid); let parent_connector = if is_last { @@ -119,19 +128,19 @@ impl Column for Tree { } // Tree doesn't support JSON - fn display_json(&self, _pid: i32) -> String { + fn display_json(&self, _pid: i64) -> String { "".to_string() } - fn find_partial(&self, _pid: i32, _keyword: &str, _content_to_lowercase: bool) -> bool { + fn find_partial(&self, _pid: i64, _keyword: &str, _content_to_lowercase: bool) -> bool { false } - fn find_exact(&self, _pid: i32, _keyword: &str, _content_to_lowercase: bool) -> bool { + fn find_exact(&self, _pid: i64, _keyword: &str, _content_to_lowercase: bool) -> bool { false } - fn sorted_pid(&self, _order: &crate::config::ConfigSortOrder) -> Vec { + fn sorted_pid(&self, _order: &crate::config::ConfigSortOrder) -> Vec { let mut root_pids = Vec::new(); for p in self.rev_tree.values() { if !self.rev_tree.contains_key(p) { @@ -142,10 +151,13 @@ impl Column for Tree { root_pids.push(*p); } } - root_pids.sort_unstable(); + // Ordered the way the sibling lists are - see `add` - so that a thread + // row whose process row `apply_visible` dropped does not sort in front + // of every process once it becomes a root of its own. + root_pids.sort_unstable_by_key(|pid| row_sort_key(*pid)); root_pids.dedup(); - fn push_pid(tree: &HashMap>, mut pids: Vec, pid: i32) -> Vec { + fn push_pid(tree: &HashMap>, mut pids: Vec, pid: i64) -> Vec { if let Some(leafs) = tree.get(&pid) { for p in leafs { pids.push(*p); @@ -165,7 +177,7 @@ impl Column for Tree { pids } - fn apply_visible(&mut self, visible_pids: &[i32]) { + fn apply_visible(&mut self, visible_pids: &[i64]) { let mut remove_pids = Vec::new(); for k in self.rev_tree.keys() { if !visible_pids.contains(k) { @@ -175,7 +187,11 @@ impl Column for Tree { for pid in remove_pids { self.rev_tree.remove(&pid); for x in self.tree.values_mut() { - if let Ok(i) = x.binary_search(&pid) { + // The siblings are ordered by `row_sort_key`, so a thread row + // has to be looked up the same way: a plain `binary_search` + // on its negated key misses it and leaves a row the filter + // dropped behind in the tree. + if let Ok(i) = x.binary_search_by_key(&row_sort_key(pid), |p| row_sort_key(*p)) { x.remove(i); } } @@ -192,8 +208,8 @@ impl Column for Tree { self.width = 0; } - fn update_width(&mut self, pid: i32, _max_width: Option) { - fn get_depth(rev_tree: &HashMap, pid: i32, depth: i32) -> i32 { + fn update_width(&mut self, pid: i64, _max_width: Option) { + fn get_depth(rev_tree: &HashMap, pid: i64, depth: i64) -> i64 { if let Some(ppid) = rev_tree.get(&pid) { if *ppid == pid { depth @@ -214,10 +230,47 @@ impl Column for Tree { crate::column_default_is_numeric!(false); } +/// Siblings are held in `row_sort_key` order, so every lookup in one of those +/// lists has to use the same key - see `add`. +#[cfg(test)] +mod tests_sorted { + use super::*; + + #[test] + fn apply_visible_drops_a_thread_row() { + let mut tree = Tree::new(&[ + String::from("│"), + String::from("─"), + String::from("┬"), + String::from("├"), + String::from("└"), + ]); + + // One parent with four children: two processes with the threads whose + // keys are the negated ids 8 and 16 in between. Ordering by key puts + // the last thread after a larger process id, which is exactly where a + // plain `binary_search` on the key stops looking. + let mut siblings = vec![4i64, -8, 12, -16]; + siblings.sort_unstable_by_key(|pid| row_sort_key(*pid)); + tree.tree.insert(0, siblings); + tree.rev_tree.insert(4, 0); + tree.rev_tree.insert(-8, 0); + tree.rev_tree.insert(12, 0); + tree.rev_tree.insert(-16, 0); + + tree.apply_visible(&[0, 4, 12]); + + assert_eq!(tree.tree[&0], vec![4, 12]); + assert!(!tree.rev_tree.contains_key(&-8)); + assert!(!tree.rev_tree.contains_key(&-16)); + } +} + #[cfg(test)] #[cfg(any(target_os = "linux", target_os = "android"))] mod tests { use super::*; + use crate::process::ProcessInfoBase; use crate::process::ProcessTask; use procfs::process::Process; use std::time::Duration; @@ -240,14 +293,12 @@ mod tests { let prev_stat = Process::myself().unwrap().stat().unwrap(); let p0 = ProcessInfo { - pid: 0, - ppid: 0, + base: ProcessInfoBase::new(0, 0, Duration::new(0, 0)), curr_proc, prev_stat, curr_io: None, prev_io: None, curr_status: None, - interval: Duration::new(0, 0), }; let curr_proc = ProcessTask::Process { @@ -258,14 +309,12 @@ mod tests { let prev_stat = Process::myself().unwrap().stat().unwrap(); let p1 = ProcessInfo { - pid: 1, - ppid: 0, + base: ProcessInfoBase::new(1, 0, Duration::new(0, 0)), curr_proc, prev_stat, curr_io: None, prev_io: None, curr_status: None, - interval: Duration::new(0, 0), }; let curr_proc = ProcessTask::Process { @@ -276,14 +325,12 @@ mod tests { let prev_stat = Process::myself().unwrap().stat().unwrap(); let p2 = ProcessInfo { - pid: 2, - ppid: 1, + base: ProcessInfoBase::new(2, 1, Duration::new(0, 0)), curr_proc, prev_stat, curr_io: None, prev_io: None, curr_status: None, - interval: Duration::new(0, 0), }; tree.add(&p0); diff --git a/src/columns/tree_slot.rs b/src/columns/tree_slot.rs index e5259b15e..e0e5f125e 100644 --- a/src/columns/tree_slot.rs +++ b/src/columns/tree_slot.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct TreeSlot { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/tty.rs b/src/columns/tty.rs index 4e3ad590e..ae30da4ea 100644 --- a/src/columns/tty.rs +++ b/src/columns/tty.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Tty { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -46,13 +46,13 @@ impl Column for Tty { #[cfg(target_os = "macos")] impl Column for Tty { fn add(&mut self, proc: &ProcessInfo) { - let dev = proc.curr_task.pbsd.e_tdev; + let dev = proc.curr_proc.kp_eproc.e_tdev; let major = (dev >> 24) & 0xff; let minor = dev & 0xffffff; let fmt_content = if major == 16 { format!("s{:03}", minor) } else { - String::from("") + String::new() }; let raw_content = fmt_content.clone(); @@ -66,7 +66,7 @@ impl Column for Tty { #[cfg(target_os = "freebsd")] impl Column for Tty { fn add(&mut self, proc: &ProcessInfo) { - let dev = proc.curr_proc.info.tdev; + let dev = proc.curr_proc.ki_tdev; let mut buf = [0u8; 256]; @@ -89,7 +89,7 @@ impl Column for Tty { let fmt_content = if let Ok(devname) = std::ffi::CStr::from_bytes_until_nul(&buf) { devname.to_string_lossy().into_owned() } else { - String::from("") + String::new() }; let raw_content = fmt_content.clone(); diff --git a/src/columns/udp_port.rs b/src/columns/udp_port.rs index 80e890cd3..a91f968a6 100644 --- a/src/columns/udp_port.rs +++ b/src/columns/udp_port.rs @@ -10,8 +10,8 @@ use std::collections::HashMap; pub struct UdpPort { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, #[cfg(any(target_os = "linux", target_os = "android"))] udp_entry: Vec, @@ -69,7 +69,7 @@ impl Column for UdpPort { self.raw_contents.insert(proc.pid, raw_content); } - fn find_exact(&self, pid: i32, keyword: &str, _content_to_lowercase: bool) -> bool { + fn find_exact(&self, pid: i64, keyword: &str, _content_to_lowercase: bool) -> bool { if let Some(content) = self.fmt_contents.get(&pid) { let content = content.replace(['[', ']'], ""); let content = content.split(','); @@ -117,7 +117,7 @@ impl Column for UdpPort { self.raw_contents.insert(proc.pid, raw_content); } - fn find_exact(&self, pid: i32, keyword: &str, _content_to_lowercase: bool) -> bool { + fn find_exact(&self, pid: i64, keyword: &str, _content_to_lowercase: bool) -> bool { if let Some(content) = self.fmt_contents.get(&pid) { let content = content.replace(['[', ']'], ""); let content = content.split(','); diff --git a/src/columns/uid.rs b/src/columns/uid.rs index de35a2365..0392f1987 100644 --- a/src/columns/uid.rs +++ b/src/columns/uid.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Uid { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, #[allow(dead_code)] abbr_sid: bool, @@ -48,7 +48,7 @@ impl Column for Uid { #[cfg(target_os = "macos")] impl Column for Uid { fn add(&mut self, proc: &ProcessInfo) { - let uid = proc.curr_task.pbsd.pbi_uid; + let uid = proc.curr_proc.kp_eproc.e_ucred.cr_uid; let fmt_content = format!("{}", uid); let raw_content = uid; @@ -82,7 +82,7 @@ impl Column for Uid { #[cfg(target_os = "freebsd")] impl Column for Uid { fn add(&mut self, proc: &ProcessInfo) { - let uid = proc.curr_proc.info.uid; + let uid = proc.curr_proc.ki_uid; let fmt_content = format!("{}", uid); let raw_content = uid; diff --git a/src/columns/uid_fs.rs b/src/columns/uid_fs.rs index be825c823..3b7a642a9 100644 --- a/src/columns/uid_fs.rs +++ b/src/columns/uid_fs.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct UidFs { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/uid_login.rs b/src/columns/uid_login.rs index d81588817..1912efc1f 100644 --- a/src/columns/uid_login.rs +++ b/src/columns/uid_login.rs @@ -8,8 +8,8 @@ const UID_NOT_SET: u32 = 0xffffffff; pub struct UidLogin { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/uid_real.rs b/src/columns/uid_real.rs index 347ca2fd5..34b8343f0 100644 --- a/src/columns/uid_real.rs +++ b/src/columns/uid_real.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct UidReal { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -45,7 +45,7 @@ impl Column for UidReal { #[cfg(target_os = "macos")] impl Column for UidReal { fn add(&mut self, proc: &ProcessInfo) { - let uid = proc.curr_task.pbsd.pbi_ruid; + let uid = proc.curr_proc.kp_eproc.e_pcred.p_ruid; let fmt_content = format!("{}", uid); let raw_content = uid; @@ -59,7 +59,7 @@ impl Column for UidReal { #[cfg(target_os = "freebsd")] impl Column for UidReal { fn add(&mut self, proc: &ProcessInfo) { - let uid = proc.curr_proc.info.ruid; + let uid = proc.curr_proc.ki_ruid; let fmt_content = format!("{}", uid); let raw_content = uid; diff --git a/src/columns/uid_saved.rs b/src/columns/uid_saved.rs index 3058b81a4..e55f7763c 100644 --- a/src/columns/uid_saved.rs +++ b/src/columns/uid_saved.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct UidSaved { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -45,7 +45,7 @@ impl Column for UidSaved { #[cfg(target_os = "macos")] impl Column for UidSaved { fn add(&mut self, proc: &ProcessInfo) { - let uid = proc.curr_task.pbsd.pbi_svuid; + let uid = proc.curr_proc.kp_eproc.e_pcred.p_svuid; let fmt_content = format!("{}", uid); let raw_content = uid; @@ -59,7 +59,7 @@ impl Column for UidSaved { #[cfg(target_os = "freebsd")] impl Column for UidSaved { fn add(&mut self, proc: &ProcessInfo) { - let uid = proc.curr_proc.info.svuid; + let uid = proc.curr_proc.ki_svuid; let fmt_content = format!("{}", uid); let raw_content = uid; diff --git a/src/columns/usage_cpu.rs b/src/columns/usage_cpu.rs index 0c8e98054..3f3567e77 100644 --- a/src/columns/usage_cpu.rs +++ b/src/columns/usage_cpu.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct UsageCpu { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -33,7 +33,7 @@ impl Column for UsageCpu { let curr_time = curr_stat.utime + curr_stat.stime; let prev_time = prev_stat.utime + prev_stat.stime; - let usage_ms = (curr_time - prev_time) * 1000 / procfs::ticks_per_second(); + let usage_ms = curr_time.saturating_sub(prev_time) * 1000 / procfs::ticks_per_second(); let interval_ms = proc.interval.as_secs() * 1000 + u64::from(proc.interval.subsec_millis()); let usage = usage_ms as f64 * 100.0 / interval_ms as f64; @@ -50,11 +50,9 @@ impl Column for UsageCpu { #[cfg(target_os = "macos")] impl Column for UsageCpu { fn add(&mut self, proc: &ProcessInfo) { - let curr_time = - proc.curr_task.ptinfo.pti_total_user + proc.curr_task.ptinfo.pti_total_system; - let prev_time = - proc.prev_task.ptinfo.pti_total_user + proc.prev_task.ptinfo.pti_total_system; - let usage_ms = (curr_time - prev_time) / 1000000u64; + let curr_time = proc.curr_task.pti_total_user + proc.curr_task.pti_total_system; + let prev_time = proc.prev_task.pti_total_user + proc.prev_task.pti_total_system; + let usage_ms = curr_time.saturating_sub(prev_time) / 1000000u64; let interval_ms = proc.interval.as_secs() * 1000 + u64::from(proc.interval.subsec_millis()); let usage = usage_ms as f64 * 100.0 / interval_ms as f64; @@ -74,7 +72,7 @@ impl Column for UsageCpu { let curr_time = proc.cpu_info.curr_sys + proc.cpu_info.curr_user; let prev_time = proc.cpu_info.prev_sys + proc.cpu_info.prev_user; - let usage_ms = (curr_time - prev_time) / 10000u64; + let usage_ms = curr_time.saturating_sub(prev_time) / 10000u64; let interval_ms = proc.interval.as_secs() * 1000 + u64::from(proc.interval.subsec_millis()); let usage = usage_ms as f64 * 100.0 / interval_ms as f64; @@ -91,11 +89,15 @@ impl Column for UsageCpu { #[cfg(target_os = "freebsd")] impl Column for UsageCpu { fn add(&mut self, proc: &ProcessInfo) { - let curr_time = (proc.curr_proc.info.rusage.utime.to_us() - + proc.curr_proc.info.rusage.stime.to_us()) as u64; - let prev_time = (proc.prev_proc.info.rusage.utime.to_us() - + proc.prev_proc.info.rusage.stime.to_us()) as u64; - let usage_ms = (curr_time - prev_time) / 1_000u64; + let curr_time = (proc.curr_proc.ki_rusage.ru_utime.tv_sec * 1_000_000 + + proc.curr_proc.ki_rusage.ru_utime.tv_usec + + proc.curr_proc.ki_rusage.ru_stime.tv_sec * 1_000_000 + + proc.curr_proc.ki_rusage.ru_stime.tv_usec) as u64; + let prev_time = (proc.prev_proc.ki_rusage.ru_utime.tv_sec * 1_000_000 + + proc.prev_proc.ki_rusage.ru_utime.tv_usec + + proc.prev_proc.ki_rusage.ru_stime.tv_sec * 1_000_000 + + proc.prev_proc.ki_rusage.ru_stime.tv_usec) as u64; + let usage_ms = curr_time.saturating_sub(prev_time) / 1_000u64; let interval_ms = proc.interval.as_secs() * 1000 + u64::from(proc.interval.subsec_millis()); let usage = usage_ms as f64 * 100.0 / interval_ms as f64; diff --git a/src/columns/usage_mem.rs b/src/columns/usage_mem.rs index 2afc35cff..c6380cd66 100644 --- a/src/columns/usage_mem.rs +++ b/src/columns/usage_mem.rs @@ -10,8 +10,8 @@ use windows_sys::Win32::System::SystemInformation::{GlobalMemoryStatusEx, MEMORY pub struct UsageMem { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, mem_total: u64, } @@ -112,7 +112,7 @@ impl Column for UsageMem { #[cfg(target_os = "macos")] impl Column for UsageMem { fn add(&mut self, proc: &ProcessInfo) { - let usage = proc.curr_task.ptinfo.pti_resident_size as f64 * 100.0 / self.mem_total as f64; + let usage = proc.curr_task.pti_resident_size as f64 * 100.0 / self.mem_total as f64; let fmt_content = format!("{:.1}", usage); let raw_content = (usage * 1000.0) as u32; @@ -140,7 +140,7 @@ impl Column for UsageMem { #[cfg(target_os = "freebsd")] impl Column for UsageMem { fn add(&mut self, proc: &ProcessInfo) { - let usage = proc.curr_proc.info.rssize as f64 * 100.0 / self.mem_total as f64; + let usage = proc.curr_proc.ki_rssize as f64 * 100.0 / self.mem_total as f64; let fmt_content = format!("{:.1}", usage); let raw_content = (usage * 1000.0) as u32; diff --git a/src/columns/user.rs b/src/columns/user.rs index c65904c62..cdb2ee294 100644 --- a/src/columns/user.rs +++ b/src/columns/user.rs @@ -10,8 +10,8 @@ use uzers::Users; pub struct User { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, #[allow(dead_code)] abbr_sid: bool, @@ -53,7 +53,7 @@ impl Column for User { #[cfg(target_os = "macos")] impl Column for User { fn add(&mut self, proc: &ProcessInfo) { - let uid = proc.curr_task.pbsd.pbi_uid; + let uid = proc.curr_proc.kp_eproc.e_ucred.cr_uid; let fmt_content = if let Some(user) = USERS_CACHE.with(|x| x.borrow_mut().get_user_by_uid(uid)) { format!("{}", user.name().to_string_lossy()) @@ -90,7 +90,7 @@ impl Column for User { #[cfg(target_os = "freebsd")] impl Column for User { fn add(&mut self, proc: &ProcessInfo) { - let uid = proc.curr_proc.info.uid; + let uid = proc.curr_proc.ki_uid; let fmt_content = if let Some(user) = USERS_CACHE.with(|x| x.borrow_mut().get_user_by_uid(uid)) { format!("{}", user.name().to_string_lossy()) diff --git a/src/columns/user_fs.rs b/src/columns/user_fs.rs index e2a99c037..3794ca5aa 100644 --- a/src/columns/user_fs.rs +++ b/src/columns/user_fs.rs @@ -8,8 +8,8 @@ use uzers::Users; pub struct UserFs { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/user_login.rs b/src/columns/user_login.rs index ca98f0250..05c81a67a 100644 --- a/src/columns/user_login.rs +++ b/src/columns/user_login.rs @@ -10,8 +10,8 @@ const UID_NOT_SET: u32 = 0xffffffff; pub struct UserLogin { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/user_real.rs b/src/columns/user_real.rs index d82f9e760..2a47b6310 100644 --- a/src/columns/user_real.rs +++ b/src/columns/user_real.rs @@ -8,8 +8,8 @@ use uzers::Users; pub struct UserReal { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -52,7 +52,7 @@ impl Column for UserReal { #[cfg(target_os = "macos")] impl Column for UserReal { fn add(&mut self, proc: &ProcessInfo) { - let uid = proc.curr_task.pbsd.pbi_ruid; + let uid = proc.curr_proc.kp_eproc.e_pcred.p_ruid; let fmt_content = if let Some(user) = USERS_CACHE.with(|x| x.borrow_mut().get_user_by_uid(uid)) { format!("{}", user.name().to_string_lossy()) @@ -71,7 +71,7 @@ impl Column for UserReal { #[cfg(target_os = "freebsd")] impl Column for UserReal { fn add(&mut self, proc: &ProcessInfo) { - let uid = proc.curr_proc.info.ruid; + let uid = proc.curr_proc.ki_ruid; let fmt_content = if let Some(user) = USERS_CACHE.with(|x| x.borrow_mut().get_user_by_uid(uid)) { format!("{}", user.name().to_string_lossy()) diff --git a/src/columns/user_saved.rs b/src/columns/user_saved.rs index 23471e03a..86121279b 100644 --- a/src/columns/user_saved.rs +++ b/src/columns/user_saved.rs @@ -8,8 +8,8 @@ use uzers::Users; pub struct UserSaved { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -52,7 +52,7 @@ impl Column for UserSaved { #[cfg(target_os = "macos")] impl Column for UserSaved { fn add(&mut self, proc: &ProcessInfo) { - let uid = proc.curr_task.pbsd.pbi_svuid; + let uid = proc.curr_proc.kp_eproc.e_pcred.p_svuid; let fmt_content = if let Some(user) = USERS_CACHE.with(|x| x.borrow_mut().get_user_by_uid(uid)) { format!("{}", user.name().to_string_lossy()) @@ -71,7 +71,7 @@ impl Column for UserSaved { #[cfg(target_os = "freebsd")] impl Column for UserSaved { fn add(&mut self, proc: &ProcessInfo) { - let uid = proc.curr_proc.info.svuid; + let uid = proc.curr_proc.ki_svuid; let fmt_content = if let Some(user) = USERS_CACHE.with(|x| x.borrow_mut().get_user_by_uid(uid)) { format!("{}", user.name().to_string_lossy()) diff --git a/src/columns/vm_data.rs b/src/columns/vm_data.rs index 2e35ea9e9..b0dd73a8d 100644 --- a/src/columns/vm_data.rs +++ b/src/columns/vm_data.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct VmData { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -50,7 +50,7 @@ impl Column for VmData { #[cfg(target_os = "freebsd")] impl Column for VmData { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = (proc.curr_proc.info.dsize as u64).saturating_mul(4096); + let raw_content = (proc.curr_proc.ki_dsize as u64).saturating_mul(4096); let fmt_content = bytify(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/vm_exe.rs b/src/columns/vm_exe.rs index b056cf4a6..71f1448d4 100644 --- a/src/columns/vm_exe.rs +++ b/src/columns/vm_exe.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct VmExe { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -50,7 +50,7 @@ impl Column for VmExe { #[cfg(target_os = "freebsd")] impl Column for VmExe { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = (proc.curr_proc.info.tsize as u64).saturating_mul(4096); + let raw_content = (proc.curr_proc.ki_tsize as u64).saturating_mul(4096); let fmt_content = bytify(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/vm_hwm.rs b/src/columns/vm_hwm.rs index 36e0b7cc8..97cd09b11 100644 --- a/src/columns/vm_hwm.rs +++ b/src/columns/vm_hwm.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct VmHwm { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -63,7 +63,7 @@ impl Column for VmHwm { #[cfg(target_os = "freebsd")] impl Column for VmHwm { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = (proc.curr_proc.info.rusage.maxrss as u64).saturating_mul(4096); + let raw_content = (proc.curr_proc.ki_rusage.ru_maxrss as u64).saturating_mul(4096); let fmt_content = bytify(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/vm_lib.rs b/src/columns/vm_lib.rs index ce3408f04..89d7ec9b7 100644 --- a/src/columns/vm_lib.rs +++ b/src/columns/vm_lib.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct VmLib { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/vm_lock.rs b/src/columns/vm_lock.rs index 8830a404d..e23dd15ca 100644 --- a/src/columns/vm_lock.rs +++ b/src/columns/vm_lock.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct VmLock { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/vm_peak.rs b/src/columns/vm_peak.rs index 27ab593f5..1270b5e35 100644 --- a/src/columns/vm_peak.rs +++ b/src/columns/vm_peak.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct VmPeak { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/vm_pin.rs b/src/columns/vm_pin.rs index a92a3a4a2..769e833c2 100644 --- a/src/columns/vm_pin.rs +++ b/src/columns/vm_pin.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct VmPin { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/vm_pte.rs b/src/columns/vm_pte.rs index cc009412b..fa56b49fc 100644 --- a/src/columns/vm_pte.rs +++ b/src/columns/vm_pte.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct VmPte { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/vm_rss.rs b/src/columns/vm_rss.rs index e2ea9cef6..c75aa77b5 100644 --- a/src/columns/vm_rss.rs +++ b/src/columns/vm_rss.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct VmRss { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -43,7 +43,7 @@ impl Column for VmRss { #[cfg(target_os = "macos")] impl Column for VmRss { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_task.ptinfo.pti_resident_size; + let raw_content = proc.curr_task.pti_resident_size; let fmt_content = bytify(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); @@ -69,7 +69,7 @@ impl Column for VmRss { #[cfg(target_os = "freebsd")] impl Column for VmRss { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = (proc.curr_proc.info.rssize as u64).saturating_mul(4096); + let raw_content = (proc.curr_proc.ki_rssize as u64).saturating_mul(4096); let fmt_content = bytify(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/vm_size.rs b/src/columns/vm_size.rs index 312faca07..e9f587d05 100644 --- a/src/columns/vm_size.rs +++ b/src/columns/vm_size.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct VmSize { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -42,7 +42,7 @@ impl Column for VmSize { #[cfg(target_os = "macos")] impl Column for VmSize { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_task.ptinfo.pti_virtual_size; + let raw_content = proc.curr_task.pti_virtual_size; let fmt_content = bytify(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); @@ -68,7 +68,9 @@ impl Column for VmSize { #[cfg(target_os = "freebsd")] impl Column for VmSize { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_proc.info.size as u64; + // `ki_size` is a `vm_size_t`, which is a `usize` in `libc`'s binding + // and only 32 bits wide on the 32-bit architectures. + let raw_content = proc.curr_proc.ki_size as u64; let fmt_content = bytify(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/vm_stack.rs b/src/columns/vm_stack.rs index a0298a1f2..832893119 100644 --- a/src/columns/vm_stack.rs +++ b/src/columns/vm_stack.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct VmStack { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -50,7 +50,7 @@ impl Column for VmStack { #[cfg(target_os = "freebsd")] impl Column for VmStack { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = (proc.curr_proc.info.ssize as u64).saturating_mul(4096); + let raw_content = (proc.curr_proc.ki_ssize as u64).saturating_mul(4096); let fmt_content = bytify(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/vm_swap.rs b/src/columns/vm_swap.rs index 75e07a7f7..cf18fbe9c 100644 --- a/src/columns/vm_swap.rs +++ b/src/columns/vm_swap.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct VmSwap { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } diff --git a/src/columns/vm_total.rs b/src/columns/vm_total.rs index 4fc283bc1..fd0cb5bb8 100644 --- a/src/columns/vm_total.rs +++ b/src/columns/vm_total.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct VmTotal { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -45,7 +45,7 @@ impl Column for VmTotal { fn add(&mut self, proc: &ProcessInfo) { let raw_content = match &proc.curr_res { Some(mem) => mem.ri_phys_footprint, - None => proc.curr_task.ptinfo.pti_resident_size, + None => proc.curr_task.pti_resident_size, }; let fmt_content = bytify(raw_content); @@ -72,7 +72,7 @@ impl Column for VmTotal { #[cfg(target_os = "freebsd")] impl Column for VmTotal { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = (proc.curr_proc.info.rssize as u64).saturating_mul(4096); + let raw_content = (proc.curr_proc.ki_rssize as u64).saturating_mul(4096); let fmt_content = bytify(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/voluntary_context_sw.rs b/src/columns/voluntary_context_sw.rs index a3e4d19ae..42ee29ec3 100644 --- a/src/columns/voluntary_context_sw.rs +++ b/src/columns/voluntary_context_sw.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct VoluntaryContextSw { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -50,7 +50,7 @@ impl Column for VoluntaryContextSw { #[cfg(target_os = "freebsd")] impl Column for VoluntaryContextSw { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = proc.curr_proc.info.rusage.nvcsw as u64; + let raw_content = proc.curr_proc.ki_rusage.ru_nvcsw as u64; let fmt_content = bytify(raw_content); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/columns/wchan.rs b/src/columns/wchan.rs index f2689a07f..06867de0f 100644 --- a/src/columns/wchan.rs +++ b/src/columns/wchan.rs @@ -6,8 +6,8 @@ use std::collections::HashMap; pub struct Wchan { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -45,10 +45,10 @@ impl Column for Wchan { #[cfg(target_os = "freebsd")] impl Column for Wchan { fn add(&mut self, proc: &ProcessInfo) { - let raw_content = if let Ok(wmesg) = crate::util::ptr_to_cstr(&proc.curr_proc.info.wmesg) { + let raw_content = if let Ok(wmesg) = crate::util::ptr_to_cstr(&proc.curr_proc.ki_wmesg) { wmesg.to_string_lossy().into_owned() } else { - String::from("") + String::new() }; let fmt_content = raw_content.clone(); diff --git a/src/columns/work_dir.rs b/src/columns/work_dir.rs index 46937f688..efd2f0df1 100644 --- a/src/columns/work_dir.rs +++ b/src/columns/work_dir.rs @@ -1,8 +1,14 @@ use crate::process::ProcessInfo; use crate::{column_default, Column}; +#[cfg(target_os = "freebsd")] +use libc::{c_void, CTL_KERN, KERN_PROC, KERN_PROC_CWD}; +#[cfg(target_os = "macos")] +use libproc::libproc::proc_pid::{PIDInfo, PidInfoFlavor, pidinfo}; use std::cmp; use std::collections::HashMap; use std::path::PathBuf; +#[cfg(target_os = "freebsd")] +use std::ptr; #[cfg(target_os = "windows")] use windows_sys::Win32::Foundation::HANDLE; @@ -10,8 +16,8 @@ use windows_sys::Win32::Foundation::HANDLE; pub struct WorkDir { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, #[allow(dead_code)] procfs: Option, @@ -39,10 +45,10 @@ impl Column for WorkDir { if let Ok(path) = proc.cwd() { path.to_string_lossy().to_string() } else { - String::from("") + String::new() } } else { - String::from("") + String::new() }; let raw_content = fmt_content.clone(); @@ -66,6 +72,170 @@ impl Column for WorkDir { column_default!(String, false); } +#[cfg(target_os = "freebsd")] +impl Column for WorkDir { + fn add(&mut self, proc: &ProcessInfo) { + let fmt_content = work_dir_of(proc.pid) + .map(|dir| dir.to_string_lossy().into_owned()) + .unwrap_or_default(); + let raw_content = fmt_content.clone(); + + self.fmt_contents.insert(proc.pid, fmt_content); + self.raw_contents.insert(proc.pid, raw_content); + } + + column_default!(String, false); +} + +/// The directory the process is sitting in, from `proc_pidinfo` with +/// `PROC_PIDVNODEPATHINFO`. +/// +/// Unlike the environment, this one is not already in hand: the collector +/// never asks for it, so it is read here, once per row, only when the column +/// is on screen. `PathInfo::root` is no substitute - that is where the +/// executable was loaded from, not where the process has since moved to. +/// +/// A directory belongs to a process, so a thread row is left blank: the row +/// stands for a thread of one. +#[cfg(target_os = "macos")] +impl Column for WorkDir { + fn add(&mut self, proc: &ProcessInfo) { + let fmt_content = if proc.pid < 0 { + String::new() + } else { + work_dir_of(proc.pid) + .map(|dir| dir.to_string_lossy().into_owned()) + .unwrap_or_default() + }; + + self.fmt_contents.insert(proc.pid, fmt_content.clone()); + self.raw_contents.insert(proc.pid, fmt_content); + } + + column_default!(String, false); +} + +/// The current working directory of `pid`, from FreeBSD's `KERN_PROC_CWD`. +/// +/// `KERN_PROC_CWD` answers with one `struct kinfo_file`, the last field of +/// which is the path. `libc::kinfo_file` carries that structure with the +/// layout the ABI pins it to - `kf_path` at `KINFO_FILE_SIZE - PATH_MAX`, +/// which is 368 of the 1392 bytes - so the answer is read through it instead +/// of by offset. +/// +/// The kernel reports the size of what it wrote in `kf_structsize`, which is +/// checked against the size of the structure before any field is read: on a +/// FreeBSD whose `kinfo_file` differs from the one `libc` describes - an +/// older release, or a future one that inserts a field ahead of the path - +/// the column comes out empty rather than reading a path out of the middle of +/// something else. +#[cfg(target_os = "freebsd")] +fn work_dir_of(pid: i64) -> Option { + // A thread row carries its thread id negated, which is not a pid and names + // no process to ask about. + if pid <= 0 { + return None; + } + + let want = std::mem::size_of::(); + let mut mib = [CTL_KERN, KERN_PROC, KERN_PROC_CWD, pid as i32]; + let mut size = 0usize; + if unsafe { + libc::sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + ptr::null_mut(), + &mut size, + ptr::null_mut(), + 0, + ) + } != 0 + || size < want + { + return None; + } + + // A list of `u64` rather than of bytes: `kinfo_file` holds 8-byte fields, + // so reading it out of a byte buffer would be a misaligned read. + let mut words = vec![0u64; size.div_ceil(std::mem::size_of::())]; + let mut size = words.len() * std::mem::size_of::(); + if unsafe { + libc::sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + words.as_mut_ptr() as *mut c_void, + &mut size, + ptr::null_mut(), + 0, + ) + } != 0 + { + return None; + } + + // SAFETY: the buffer is 8-byte aligned, holds at least one `kinfo_file`, + // and is still alive. Only `kf_structsize` is read before the layout is + // confirmed to be the one this build was compiled against. + let kf = unsafe { &*words.as_ptr().cast::() }; + if kf.kf_structsize as usize != want { + return None; + } + + // SAFETY: `kf_path` is a `PATH_MAX` array the kernel filled; the slice is + // bounded by the array itself, so a missing NUL cannot run past it. + let raw = unsafe { + std::slice::from_raw_parts(kf.kf_path.as_ptr().cast::(), kf.kf_path.len()) + }; + let end = raw.iter().position(|byte| *byte == 0).unwrap_or(raw.len()); + if end == 0 { + return None; + } + + Some(PathBuf::from( + String::from_utf8_lossy(&raw[..end]).into_owned(), + )) +} + +/// The current working directory of `pid`. +/// +/// `KERN_PROCARGS2` carries the arguments and the environment, but not the +/// directory a process sits in, so `proc_pidinfo` with +/// `PROC_PIDVNODEPATHINFO` is the only source for it. The kernel answers for +/// the processes of the user `procs` runs as and refuses the rest - the wall +/// the rest of `libproc` hits too - so `None` here means "not ours to see" +/// far more often than it means "no directory". +/// +/// Note that this is not what `PathInfo::root` holds: that field is the +/// directory the executable was loaded from, `proc_pidpath` territory, and a +/// process that has changed directory since keeps pointing at the old one. +#[cfg(target_os = "macos")] +fn work_dir_of(pid: i64) -> Option { + // 0 is the kernel task and has no vnode paths; the negative pids are the + // synthetic keys the thread rows carry. + if pid <= 0 { + return None; + } + + let info = pidinfo::(pid as i32, 0).ok()?; + // libc declares `vip_path` as `[[c_char; 32]; 32]`, not `[c_char; MAXPATHLEN]`. + let path = crate::util::ptr_to_cstr(info.0.pvi_cdir.vip_path.as_flattened()).ok()?; + Some(PathBuf::from(path.to_string_lossy().into_owned())) +} + +/// libproc's `pidinfo` passes `&mut T` to `proc_pidinfo` sized by +/// `size_of::()`, so this must keep the exact layout of +/// `proc_vnodepathinfo`. +#[cfg(target_os = "macos")] +#[repr(transparent)] +struct VNodePathInfo(libc::proc_vnodepathinfo); + +#[cfg(target_os = "macos")] +impl PIDInfo for VNodePathInfo { + fn flavor() -> PidInfoFlavor { + PidInfoFlavor::VNodePathInfo + } +} + /// Reads the current working directory of `pid` from its PEB. /// /// The PEB address comes from `NtQueryInformationProcess` with @@ -74,7 +244,7 @@ impl Column for WorkDir { /// `PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ`, so protected /// processes (e.g. PPL) yield `None`. #[cfg(target_os = "windows")] -fn work_dir_of(pid: i32) -> Option { +fn work_dir_of(pid: i64) -> Option { use windows_sys::Win32::Foundation::{CloseHandle, FALSE, HANDLE}; use windows_sys::Win32::System::Threading::{ OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION, PROCESS_VM_READ, @@ -107,8 +277,9 @@ fn work_dir_of(pid: i32) -> Option { #[cfg(target_os = "windows")] fn read_work_dir(handle: HANDLE) -> Option { use windows_sys::Win32::Foundation::UNICODE_STRING; + use windows_sys::Win32::System::Threading::PEB; use crate::process::{ - process_peb_address, read_process_memory, PEB_PREFIX, + process_peb_address, read_process_memory, RTL_USER_PROCESS_PARAMETERS_PREFIX, }; use std::mem::{offset_of, size_of}; @@ -128,7 +299,7 @@ fn read_work_dir(handle: HANDLE) -> Option { peb_buf .as_ptr() .cast::() - .add(offset_of!(PEB_PREFIX, ProcessParameters)) + .add(offset_of!(PEB, ProcessParameters)) .cast::() .read() }; @@ -178,3 +349,31 @@ fn read_work_dir(handle: HANDLE) -> Option { /// allocation. Real working directories are capped well below this. #[cfg(target_os = "windows")] const MAX_WORK_DIR_BYTES: usize = 64 * 1024; + +#[cfg(all(test, target_os = "macos"))] +mod tests { + use super::*; + + /// The kernel and the process agree about where the process is. + /// + /// The two paths are canonicalized because the kernel reports the real + /// path - `/private/tmp` rather than `/tmp` - while the standard library + /// is free to report either. + #[test] + fn work_dir_of_self() { + let cwd = std::env::current_dir().unwrap().canonicalize().unwrap(); + let dir = work_dir_of(std::process::id() as i64) + .unwrap() + .canonicalize() + .unwrap(); + assert_eq!(dir, cwd); + } + + /// Pids there is nothing to ask about: 0 is the kernel task, and the + /// negative keys the thread rows carry are not pids at all. + #[test] + fn work_dir_of_an_unaskable_pid() { + assert_eq!(work_dir_of(0), None); + assert_eq!(work_dir_of(-1), None); + } +} diff --git a/src/columns/write_bytes.rs b/src/columns/write_bytes.rs index c4dda91dd..12c2f0cc5 100644 --- a/src/columns/write_bytes.rs +++ b/src/columns/write_bytes.rs @@ -7,8 +7,8 @@ use std::collections::HashMap; pub struct WriteBytes { header: String, unit: String, - fmt_contents: HashMap, - raw_contents: HashMap, + fmt_contents: HashMap, + raw_contents: HashMap, width: usize, } @@ -49,16 +49,16 @@ impl Column for WriteBytes { #[cfg(target_os = "macos")] impl Column for WriteBytes { fn add(&mut self, proc: &ProcessInfo) { - let (fmt_content, raw_content) = if proc.curr_res.is_some() && proc.prev_res.is_some() { - let interval_ms = proc.interval.as_secs() * 1000 + u64::from(proc.interval.subsec_millis()); - let io = (proc.curr_res.as_ref().unwrap().ri_diskio_byteswritten - - proc.prev_res.as_ref().unwrap().ri_diskio_byteswritten) - * 1000 - / interval_ms; - (bytify(io), io) - } else { - (String::from(""), 0) - }; + let (fmt_content, raw_content) = + if let (Some(curr), Some(prev)) = (proc.curr_res.as_ref(), proc.prev_res.as_ref()) { + let interval_ms = + proc.interval.as_secs() * 1000 + u64::from(proc.interval.subsec_millis()); + let io = + (curr.ri_diskio_byteswritten - prev.ri_diskio_byteswritten) * 1000 / interval_ms; + (bytify(io), io) + } else { + (String::new(), 0) + }; self.fmt_contents.insert(proc.pid, fmt_content); self.raw_contents.insert(proc.pid, raw_content); @@ -89,8 +89,8 @@ impl Column for WriteBytes { // io block size: 128KB let block_size = 128 * 1024; let interval_ms = proc.interval.as_secs() * 1000 + u64::from(proc.interval.subsec_millis()); - let io = (proc.curr_proc.info.rusage.oublock as u64 - - proc.prev_proc.info.rusage.oublock as u64) + let io = (proc.curr_proc.ki_rusage.ru_oublock as u64 + - proc.prev_proc.ki_rusage.ru_oublock as u64) * block_size * 1000 / interval_ms; diff --git a/src/config.rs b/src/config.rs index 9a99e7c3b..88c3daac4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,7 @@ use crate::column::Column; use crate::columns::ConfigColumnKind; use serde_derive::{Deserialize, Serialize}; +use std::fmt; use std::str::FromStr; // --------------------------------------------------------------------------------------------------------------------- @@ -519,6 +520,98 @@ pub enum ConfigSearchCase { Sensitive, } +/// Which users' processes are listed. +/// +/// The value names a user: `"all"` for every one of them, `"myself"` for the +/// user `procs` runs as, or a user to keep - a name, a uid in decimal, or, on +/// Windows, a SID such as `S-1-5-21-...-1001`. TOML has no `null` to write, so +/// the value that stands for "every user" is `"all"` or the key left out; the +/// deserializer accepts `null` as well, for formats that have it. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub enum ConfigUserFilter { + /// Every user's processes. + #[default] + All, + /// Only the processes of the user `procs` itself runs as. + Myself, + /// Only the processes of the user this names. + /// + /// Which user that is - and whether there is one at all - is decided when + /// the filter is built, because only the platform knows how to look a user + /// up: a name through the user database, a number as a uid where there are + /// uids, a SID where there are SIDs. + User(String), +} + +impl ConfigUserFilter { + /// The value as it is written in the configuration file and on the command + /// line: the two keywords, or a user to look up later. + pub fn user(s: &str) -> Self { + match s.to_ascii_lowercase().as_str() { + "all" => ConfigUserFilter::All, + "myself" => ConfigUserFilter::Myself, + _ => ConfigUserFilter::User(String::from(s)), + } + } +} + +impl serde::Serialize for ConfigUserFilter { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + ConfigUserFilter::All => serializer.serialize_str("all"), + ConfigUserFilter::Myself => serializer.serialize_str("myself"), + ConfigUserFilter::User(user) => serializer.serialize_str(user), + } + } +} + +impl<'de> serde::Deserialize<'de> for ConfigUserFilter { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct UserFilterVisitor; + + impl<'de> serde::de::Visitor<'de> for UserFilterVisitor { + type Value = ConfigUserFilter; + + fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("\"all\", \"myself\", a user, or null") + } + + /// `null`, which TOML cannot write but another format can. + fn visit_unit(self) -> Result { + Ok(ConfigUserFilter::All) + } + + fn visit_none(self) -> Result { + Ok(ConfigUserFilter::All) + } + + /// A uid written as a number rather than as a string. It is kept + /// as the user it numbers, and the platform decides whether that + /// means a uid, or - on Windows, which has no uid - a user name + /// that most likely does not exist. + fn visit_u64(self, v: u64) -> Result { + Ok(ConfigUserFilter::User(v.to_string())) + } + + fn visit_i64(self, v: i64) -> Result { + Ok(ConfigUserFilter::User(v.to_string())) + } + + fn visit_str(self, v: &str) -> Result { + Ok(ConfigUserFilter::user(v)) + } + } + + deserializer.deserialize_any(UserFilterVisitor) + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ConfigDisplay { #[serde(default = "default_false")] @@ -559,6 +652,8 @@ pub struct ConfigDisplay { pub theme: ConfigTheme, #[serde(default = "default_true")] pub show_kthreads: bool, + #[serde(default)] + pub show_user_only: ConfigUserFilter, } impl Default for ConfigDisplay { @@ -589,6 +684,7 @@ impl Default for ConfigDisplay { abbr_sid: true, theme: ConfigTheme::Auto, show_kthreads: true, + show_user_only: ConfigUserFilter::All, } } } diff --git a/src/main.rs b/src/main.rs index 9a51b686d..a42e6b67d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -310,6 +310,90 @@ mod tests { assert!(ret.is_ok()); } + #[test] + fn test_only_config_keys() { + let config: Config = toml::from_str(CONFIG_DEFAULT).unwrap(); + assert_eq!(config.display.show_user_only, ConfigUserFilter::All); + + let s = format!("{CONFIG_DEFAULT}\n[display]\nshow_user_only = \"all\"\n"); + let config: Config = toml::from_str(&s).unwrap(); + assert_eq!(config.display.show_user_only, ConfigUserFilter::All); + + let s = format!("{CONFIG_DEFAULT}\n[display]\nshow_user_only = \"myself\"\n"); + let config: Config = toml::from_str(&s).unwrap(); + assert_eq!(config.display.show_user_only, ConfigUserFilter::Myself); + + // A uid, written as a number and as a string, is the same user. + let s = format!("{CONFIG_DEFAULT}\n[display]\nshow_user_only = 1000\n"); + let config: Config = toml::from_str(&s).unwrap(); + assert_eq!( + config.display.show_user_only, + ConfigUserFilter::User(String::from("1000")) + ); + + let s = format!("{CONFIG_DEFAULT}\n[display]\nshow_user_only = \"1000\"\n"); + let config: Config = toml::from_str(&s).unwrap(); + assert_eq!( + config.display.show_user_only, + ConfigUserFilter::User(String::from("1000")) + ); + + // A name is kept as it was written - it is only looked up later. + let s = format!("{CONFIG_DEFAULT}\n[display]\nshow_user_only = \"someuser\"\n"); + let config: Config = toml::from_str(&s).unwrap(); + assert_eq!( + config.display.show_user_only, + ConfigUserFilter::User(String::from("someuser")) + ); + } + + #[test] + fn test_show_user_only_unknown_user() { + let mut config: Config = toml::from_str(CONFIG_DEFAULT).unwrap(); + config.pager.mode = ConfigPagerMode::Disable; + config.display.theme = ConfigTheme::Dark; + config.display.show_user_only = ConfigUserFilter::User(String::from("no-such-user-42")); + + let args = ["procs"]; + let mut opt = Opt::parse_from(args.iter()); + let err = run_default(&mut opt, &config).unwrap_err(); + assert!(err.to_string().contains("no such user")); + } + + #[test] + fn test_run_show_user_only() { + let mut config: Config = toml::from_str(CONFIG_DEFAULT).unwrap(); + config.pager.mode = ConfigPagerMode::Disable; + config.display.theme = ConfigTheme::Dark; + config.display.show_user_only = ConfigUserFilter::Myself; + + let args = ["procs"]; + let mut opt = Opt::parse_from(args.iter()); + let ret = run_default(&mut opt, &config); + assert!(ret.is_ok()); + + let args = ["procs", "--user"]; + let opt = Opt::parse_from(args.iter()); + assert_eq!(opt.show_user_only.as_deref(), Some("myself")); + + let args = ["procs", "--user", "all"]; + let opt = Opt::parse_from(args.iter()); + assert_eq!(opt.show_user_only.as_deref(), Some("all")); + + let args = ["procs", "--user", "1000"]; + let opt = Opt::parse_from(args.iter()); + assert_eq!(opt.show_user_only.as_deref(), Some("1000")); + + // The short form, which takes its value the same way. + let args = ["procs", "-u"]; + let opt = Opt::parse_from(args.iter()); + assert_eq!(opt.show_user_only.as_deref(), Some("myself")); + + let args = ["procs", "-u", "all"]; + let opt = Opt::parse_from(args.iter()); + assert_eq!(opt.show_user_only.as_deref(), Some("all")); + } + #[test] fn test_run_gen_config() { let ret = run_gen_config(); diff --git a/src/opt.rs b/src/opt.rs index 91628faaa..648b67ef3 100644 --- a/src/opt.rs +++ b/src/opt.rs @@ -192,8 +192,21 @@ pub struct Opt { #[clap(long = "no-header")] pub no_header: bool, + /// Show only the processes of one user ("all", "myself", or a user) + #[clap( + short = 'u', + long = "user", + value_name = "user", + num_args = 0..=1, + default_missing_value = "myself" + )] + pub show_user_only: Option, + /// Path to procfs - #[clap(long = "procfs")] + // Linux only: on the other platforms the option is not registered at all, + // so passing it is reported as an unknown argument. + #[cfg_attr(target_os = "linux", clap(long = "procfs"))] + #[cfg_attr(not(target_os = "linux"), clap(skip))] pub procfs: Option, /// Show debug message diff --git a/src/process.rs b/src/process.rs index bdac9b168..d8f3f4d1e 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,3 +1,166 @@ +#[cfg(not(target_os = "windows"))] +use crate::config::ConfigUserFilter; +#[cfg(not(target_os = "windows"))] +use crate::util::USERS_CACHE; +#[cfg(not(target_os = "windows"))] +use anyhow::{Error, anyhow}; +use std::time::Duration; +#[cfg(not(target_os = "windows"))] +use uzers::Users; + +/// The user a filter keeps, as each platform names one. +/// +/// A uid where there are uids, and on Windows - which has none - the whole +/// SID, so that the comparison is exact rather than on the last sub-authority +/// alone. +#[cfg(target_os = "windows")] +pub type UserId = ntapi::SID_MAX; +#[cfg(not(target_os = "windows"))] +pub type UserId = u32; + +/// What the listing shows, decided while the processes are collected. +/// +/// This is handed to `collect_proc` instead of being applied to its result: +/// enumerating every process cannot be avoided - that is what tells us which +/// ones to keep - but everything a platform does per process afterwards can be +/// skipped for the ones the filter drops. On Windows that is an `OpenProcess` +/// and a command line read per process, on Linux a handful of `/proc/` +/// reads, on macOS the descriptor and thread walks. +#[derive(Clone, Copy, Default)] +pub struct ShowFilter { + /// The only user whose processes are kept, or `None` to keep every user's. + /// + /// The user the configuration names is resolved to this before the filter + /// is built - see [`user_id_of`] - so a platform is never handed a name to + /// look up, and every comparison it makes is between two of the same kind + /// of identifier. + pub userid: Option, + /// Whether kernel threads are shown. + pub kthread: bool, +} + +/// The user a filter keeps, looked up from what the configuration names. +/// +/// A number is a uid and is taken as it is: a uid can own processes without +/// having an entry in the user database at all, so there is nothing to check +/// it against. A name has to be one, because a name that resolves to nothing +/// is a typo, and a listing filtered by a typo comes out empty rather than +/// wrong - empty with a reason beats empty without one. +/// +/// `myself` answers with the *effective* uid, which is what everything the +/// filter compares against already is: the owner of `/proc/` on Linux, +/// `ki_uid` and `e_ucred.cr_uid` on the BSDs, and the value `KERN_PROC_UID` +/// selects by - all of them the effective one, as `ps` selects by too. The +/// real uid instead would only look right while the two are equal; they come +/// apart for a setuid `procs`, where the real uid is still the caller's while +/// the effective one is root's. +/// +/// This also keeps the filter agreeing with the `User` column, which shows +/// that same effective owner. +#[cfg(not(target_os = "windows"))] +pub fn user_id_of(user: &ConfigUserFilter) -> Result, Error> { + Ok(match user { + ConfigUserFilter::All => None, + ConfigUserFilter::Myself => Some(uzers::get_effective_uid()), + ConfigUserFilter::User(user) => Some(match user.parse::() { + Ok(uid) => uid, + Err(_) => USERS_CACHE + .with(|x| x.borrow_mut().get_user_by_name(user).map(|u| u.uid())) + .ok_or_else(|| anyhow!("no such user: {user}"))?, + }), + }) +} + +/// What every platform's `ProcessInfo` is made of before its own fields are +/// added: the row key, the parent, and the sampling window. +/// +/// The rest of a row - the CPU times, the memory, the command line - is +/// whatever that platform's kernel hands out and none of it is shared, so each +/// platform keeps a `ProcessInfo` of its own with this one in its `base` field +/// and dereferences to it. That is what lets a column write `proc.pid` +/// without knowing which platform it was compiled for, and it is why the +/// fields here are only the ones no platform can do without. +pub struct ProcessInfoBase { + /// The row key: a process id when it is positive, and the negated thread + /// id of a thread row when it is negative. + /// + /// Threads and processes share this one field because every column keys + /// its contents by it. Thread ids are handed out of the same pool as + /// process ids, so a thread row could otherwise be taken for a process - + /// or for a thread of another process - while a recycled id is in use + /// elsewhere. Negating the id cannot collide with a process id, and the + /// Pid column turns it back into the thread id. + /// + /// A row is a process when the key is not negative - `pid >= 0` - and a + /// thread otherwise. The sign is read where it is needed rather than + /// behind a method, so the encoding has no second spelling to keep in + /// step. + pub pid: i64, + /// The key of the process this row hangs under: a parent pid for a + /// process, and the pid of the process it belongs to for a thread. + pub ppid: i64, + /// How long the two samples behind this row are apart. + /// + /// Every rate column divides by it - bytes per second, CPU percent - so + /// it is the measured distance between the snapshots rather than the + /// interval that was asked for. + pub interval: Duration, +} + +impl ProcessInfoBase { + pub fn new(pid: i64, ppid: i64, interval: Duration) -> Self { + Self { + pid, + ppid, + interval, + } + } +} + +/// Dereferences a platform's `ProcessInfo` to its [`ProcessInfoBase`]. +/// +/// Written once so all four platforms read the same: a column says `proc.pid`, +/// not `proc.base.pid`, and the platform structs stay free to lay the rest of +/// the row out as their kernel reports it. +macro_rules! process_info_deref { + () => { + impl ::std::ops::Deref for ProcessInfo { + type Target = $crate::process::ProcessInfoBase; + + fn deref(&self) -> &Self::Target { + &self.base + } + } + }; +} + +/// The row key of a thread: its thread id, negated. +/// +/// Every column keys its contents by `pid`, the tree keys parent and child +/// rows by it, and so does the filter - so a thread needs a key of its own +/// that can be told apart from the process it belongs to and from every +/// other thread. Process ids are never negative, so negating the thread id +/// gives one, and the key still says which thread it is: negating it back +/// gives the id. +pub fn thread_key(tid: u64) -> i64 { + // An id that does not fit has nothing to be negated into; it is folded + // onto the largest one rather than skipped, and no kernel hands one out. + -i64::try_from(tid).unwrap_or(i64::MAX) +} + +/// The key a row is ordered by: the id it stands for, and whether that id +/// belongs to a process or to the thread negated into the key. +/// +/// Sorting rows by their key would sink every thread below every process, so +/// the Pid column and the tree order by this instead: a thread then lands +/// next to the process whose id it was handed - the first thread of a process +/// is given the process' own id - and on such a tie the process comes first. +pub fn row_sort_key(pid: i64) -> (u64, bool) { + // `unsigned_abs` and not `abs`: `i64::MIN` has no positive counterpart to + // be negated into, and no kernel hands out such an id anyway. + (pid.unsigned_abs(), pid < 0) +} + #[cfg(target_os = "freebsd")] pub mod freebsd; #[cfg(any(target_os = "linux", target_os = "android"))] @@ -17,3 +180,30 @@ pub use self::linux::*; pub use self::macos::*; #[cfg(target_os = "windows")] pub use self::windows::*; + +#[cfg(test)] +mod tests { + use super::{row_sort_key, thread_key}; + + /// A thread row is keyed by its negated id, and negating the key back + /// gives the id - which is what the Pid column reports. Pid 0 is a + /// process - Idle on Windows, `kernel_task` on macOS - so its key is + /// left at 0 and read back as a process; no kernel hands out thread 0. + #[test] + fn thread_key_negates_the_id() { + assert_eq!(thread_key(1), -1); + assert_eq!(-thread_key(1), 1); + assert_eq!(thread_key(0), 0); + assert_eq!(thread_key(u64::MAX), -i64::MAX); + assert!(thread_key(u64::from(u32::MAX)) < 0); + } + + /// A thread row sorts by the id it stands for, next to - and just after - + /// the process that was handed the same number. + #[test] + fn row_sort_key_orders_by_the_id_behind_the_key() { + let mut pids = [-1i64, 2, -2, 0, 1]; + pids.sort_by_key(|pid| row_sort_key(*pid)); + assert_eq!(pids, [0, 1, -1, 2, -2]); + } +} diff --git a/src/process/freebsd.rs b/src/process/freebsd.rs index 17895e8e3..66d5ed0b4 100644 --- a/src/process/freebsd.rs +++ b/src/process/freebsd.rs @@ -1,55 +1,147 @@ -use bsd_kvm::{Access, KernProc, Kvm, Process}; -use std::collections::HashMap; +use crate::process::{ProcessInfoBase, ShowFilter, thread_key}; +use libc::{ + CTL_KERN, KERN_PROC, KERN_PROC_INC_THREAD, KERN_PROC_PROC, KERN_PROC_UID, P_KPROC, c_void, + kinfo_proc, +}; +use std::collections::{HashMap, HashSet}; use std::path::PathBuf; +use std::ptr; use std::thread; use std::time::{Duration, Instant}; +/// The two `pri_class` values `State` reads, from `sys/priority.h`. +/// +/// `libc` binds the `struct priority` they live in but not the class +/// constants themselves, and they are the ABI - `PRI_ITHD` 1, `PRI_REALTIME` +/// 2, `PRI_TIMESHARE` 3, `PRI_IDLE` 4 - so the numbers are written out here +/// rather than guessed at the use site. +pub const PRI_REALTIME: u8 = 2; +pub const PRI_IDLE: u8 = 4; + pub struct ProcessInfo { - pub pid: i32, - pub ppid: i32, - pub curr_proc: Process, - pub prev_proc: Process, - pub interval: Duration, + /// The part of a row every platform has - the row key, the parent and the + /// sampling window. The `Deref` below hands it out, so a column goes on + /// writing `proc.pid` without reaching for `base`. + pub base: ProcessInfoBase, + /// The kernel's `struct kinfo_proc`, as `libc` binds it. + /// + /// FreeBSD pins the layout of this structure by size - `KINFO_PROC_SIZE`, + /// 1088 bytes on the 64-bit architectures - so the ABI is fixed even + /// though fields keep being carved out of the `ki_spare*` arrays. That is + /// what makes `libc`'s binding safe to read directly: it was checked + /// against the real headers on FreeBSD 15.1, where all 54 of the sizes + /// and field offsets this crate reads agree exactly. + pub curr_proc: kinfo_proc, + pub prev_proc: kinfo_proc, +} + +process_info_deref!(); + +fn get_processes(with_thread: bool, uid: Option) -> Vec { + let proc_selector = if uid.is_some() { + KERN_PROC_UID + } else { + KERN_PROC_PROC + } | if with_thread { KERN_PROC_INC_THREAD } else { 0 }; + // `INC_THREAD` makes the kernel emit the same process once per thread, flat. + let mut mib = vec![CTL_KERN, KERN_PROC, proc_selector]; + if let Some(uid) = uid { + mib.push(uid as libc::c_int); + } + let mut size = 0usize; + if unsafe { + libc::sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + ptr::null_mut(), + &mut size, + ptr::null_mut(), + 0, + ) + } != 0 + || size == 0 + { + return Vec::new(); + } + + let count = size.div_ceil(std::mem::size_of::()); + let mut processes: Vec = vec![unsafe { std::mem::zeroed() }; count]; + if unsafe { + libc::sysctl( + mib.as_mut_ptr(), + mib.len() as u32, + processes.as_mut_ptr() as *mut c_void, + &mut size, + ptr::null_mut(), + 0, + ) + } != 0 + { + return Vec::new(); + } + processes.truncate(size / std::mem::size_of::()); + processes +} + +/// The first row a pid appears on is the process itself and every one after +/// it is a thread of that process: FreeBSD reports both in the same walk, and +/// a tid never equals its pid, so the id cannot be used to tell them apart. +fn row_key(proc: &kinfo_proc, with_thread: bool, process_pids: &mut HashSet) -> (i64, i64) { + if with_thread && !process_pids.insert(proc.ki_pid) { + (thread_key(proc.ki_tid as u64), proc.ki_pid as i64) + } else { + (proc.ki_pid as i64, proc.ki_ppid as i64) + } } pub fn collect_proc( interval: Duration, - _with_thread: bool, - show_kthreads: bool, + with_thread: bool, _procfs_path: &Option, + filter: ShowFilter, ) -> Vec { let mut base_procs = HashMap::new(); let mut ret = Vec::new(); + let mut process_pids = HashSet::new(); - let kvm = Kvm::open(None, Some("/dev/null"), Access::ReadOnly); - if let Ok(mut kvm) = kvm { - for proc in kvm.get_process(KernProc::Proc, 0) { - if !show_kthreads && proc.info.flag & 0x00000004 /* P_KPROC */ != 0 { - continue; - } + let uid = filter.userid; + for proc in get_processes(with_thread, uid) { + if !filter.kthread && proc.ki_flag & (P_KPROC as i64) != 0 { + continue; + } + + let time = Instant::now(); + let (key, _) = row_key(&proc, with_thread, &mut process_pids); + base_procs.insert(key, (proc, time)); + } + + thread::sleep(interval); + // On FreeBSD, a tid never equals the pid, so `tid == pid` cannot be used to + // identify the main thread. Instead, the main thread is identified by the + // first occurrence of a process with a given pid. + let mut process_pids = HashSet::new(); - let time = Instant::now(); - base_procs.insert(proc.info.pid, (proc.clone(), time)); + for proc in get_processes(with_thread, uid) { + if !filter.kthread && proc.ki_flag & (P_KPROC as i64) != 0 { + continue; } - thread::sleep(interval); - - for proc in kvm.get_process(KernProc::Proc, 0) { - let pid = proc.info.pid; - if let Some((prev_proc, prev_time)) = base_procs.remove(&pid) { - let curr_time = Instant::now(); - let curr_proc = proc.clone(); - let interval = curr_time - prev_time; - - let proc = ProcessInfo { - pid: proc.info.pid, - ppid: proc.info.ppid, - curr_proc, - prev_proc, - interval, - }; - ret.push(proc); + let (key, ppid) = row_key(&proc, with_thread, &mut process_pids); + if let Some((prev_proc, prev_time)) = base_procs.remove(&key) { + if prev_proc.ki_start != proc.ki_start { + // Pid recycled + continue; } + + let curr_time = Instant::now(); + let interval = curr_time - prev_time; + + let proc = ProcessInfo { + base: ProcessInfoBase::new(key, ppid, interval), + curr_proc: proc, + prev_proc, + }; + ret.push(proc); } } diff --git a/src/process/linux.rs b/src/process/linux.rs index acbd08867..fac13ef55 100644 --- a/src/process/linux.rs +++ b/src/process/linux.rs @@ -1,3 +1,4 @@ +use crate::process::{ProcessInfoBase, ShowFilter, thread_key}; use procfs::ProcError; use procfs::ProcessCGroup; use procfs::process::{FDInfo, Io, Process, Stat, StatFlags, Status, TasksIter}; @@ -70,25 +71,43 @@ impl ProcessTask { } pub struct ProcessInfo { - pub pid: i32, - pub ppid: i32, + /// The part of a row every platform has - the row key, the parent and the + /// sampling window. The `Deref` below hands it out, so a column goes on + /// writing `proc.pid` without reaching for `base`. + /// + /// The key is a process id when it is positive and the negated thread id + /// of a thread row when it is negative: a task's id lives in the same + /// namespace as a process id, so it can stand for a row of its own - but + /// only while no process is using the same number, and a recycled id is + /// indistinguishable from the one that held it before. Negating it says + /// "this row is a thread" and leaves the Pid column free to print the id + /// it came from. + pub base: ProcessInfoBase, pub curr_proc: ProcessTask, pub prev_stat: Stat, pub curr_io: Option, pub prev_io: Option, pub curr_status: Option, - pub interval: Duration, } +process_info_deref!(); + +/// Hands the processes out one at a time instead of collecting them first. +/// +/// A `ProcessInfo` owns an open `/proc/` directory, so returning the whole +/// listing as a `Vec` means holding one descriptor per process at once. The +/// caller consumes each entry before the next one is read (see `View::new`), +/// which keeps the peak at a single descriptor no matter how many processes +/// there are - otherwise a low `RLIMIT_NOFILE` makes the second pass fail +/// halfway and the processes it could not open disappear without a word. pub fn collect_proc( interval: Duration, with_thread: bool, - show_kthreads: bool, procfs_path: &Option, -) -> Vec { + filter: ShowFilter, +) -> impl Iterator + '_ { let mut base_procs = Vec::new(); let mut base_tasks = HashMap::new(); - let mut ret = Vec::new(); let all_proc = if let Some(x) = procfs_path { procfs::process::all_processes_with_root(x) @@ -99,6 +118,20 @@ pub fn collect_proc( if let Ok(all_proc) = all_proc { for proc in all_proc.flatten() { if let Ok(stat) = proc.stat() { + // The owner is read only when something is compared against + // it: with no uid to filter on, it is a `/proc/` open per + // process for nothing. + let owner = if filter.userid.is_some() { + proc.uid().ok() + } else { + None + }; + if let Some(userid) = filter.userid + && owner != Some(userid) + { + continue; + } + let io = proc.io().ok(); let time = Instant::now(); if with_thread && let Ok(iter) = proc.tasks() { @@ -111,85 +144,82 @@ pub fn collect_proc( thread::sleep(interval); - for (pid, prev_stat, prev_io, prev_time) in base_procs { - let curr_proc = if let Ok(proc) = crate::util::process_new(pid, procfs_path) { - proc - } else { - continue; - }; + base_procs + .into_iter() + .filter_map(move |(pid, prev_stat, prev_io, prev_time)| { + let curr_proc = crate::util::process_new(pid.into(), procfs_path).ok()?; + let curr_stat = curr_proc.stat().ok()?; - let curr_stat = if let Ok(stat) = curr_proc.stat() { - stat - } else { - continue; - }; + if prev_stat.starttime != curr_stat.starttime { + // Pid recycled + return None; + } - let curr_owner = if let Ok(owner) = curr_proc.uid() { - owner - } else { - continue; - }; + let curr_owner = curr_proc.uid().ok()?; - let curr_io = curr_proc.io().ok(); - let curr_status = curr_proc.status().ok(); - let curr_time = Instant::now(); - let interval = curr_time - prev_time; - let ppid = curr_stat.ppid; - - if !show_kthreads - && curr_stat - .flags() - .unwrap_or(StatFlags::empty()) - .contains(StatFlags::PF_KTHREAD) - { - continue; - } + if let Some(userid) = filter.userid + && curr_owner != userid + { + return None; + } - let mut curr_tasks = HashMap::new(); - if with_thread && let Ok(iter) = curr_proc.tasks() { - collect_task(iter, &mut curr_tasks); - } + let curr_io = curr_proc.io().ok(); + let curr_status = curr_proc.status().ok(); + let curr_time = Instant::now(); + let interval = curr_time - prev_time; + let ppid = curr_stat.ppid; + + if !filter.kthread + && curr_stat + .flags() + .unwrap_or(StatFlags::empty()) + .contains(StatFlags::PF_KTHREAD) + { + return None; + } - let curr_proc = ProcessTask::Process { - stat: curr_stat, - owner: curr_owner, - proc: curr_proc, - }; + let mut curr_tasks = HashMap::new(); + if with_thread && let Ok(iter) = curr_proc.tasks() { + collect_task(iter, &mut curr_tasks); + } - let proc = ProcessInfo { - pid, - ppid, - curr_proc, - prev_stat, - curr_io, - prev_io, - curr_status, - interval, - }; + let curr_proc = ProcessTask::Process { + stat: curr_stat, + owner: curr_owner, + proc: curr_proc, + }; - ret.push(proc); - - for (tid, (pid, curr_stat, curr_status, curr_io)) in curr_tasks { - if let Some((_, prev_stat, _, prev_io)) = base_tasks.remove(&tid) { - let proc = ProcessInfo { - pid: tid, - ppid: pid, - curr_proc: ProcessTask::Task { - stat: curr_stat, - owner: curr_owner, - }, - prev_stat, - curr_io, - prev_io, - curr_status, - interval, - }; - ret.push(proc); + let proc = ProcessInfo { + base: ProcessInfoBase::new(pid as i64, ppid as i64, interval), + curr_proc, + prev_stat, + curr_io, + prev_io, + curr_status, + }; + + let mut tasks = Vec::new(); + + for (tid, (pid, curr_stat, curr_status, curr_io)) in curr_tasks { + if let Some((_, prev_stat, _, prev_io)) = base_tasks.remove(&tid) { + let proc = ProcessInfo { + base: ProcessInfoBase::new(thread_key(tid as u64), pid as i64, interval), + curr_proc: ProcessTask::Task { + stat: curr_stat, + owner: curr_owner, + }, + prev_stat, + curr_io, + prev_io, + curr_status, + }; + tasks.push(proc); + } } - } - } - ret + Some(std::iter::once(proc).chain(tasks)) + }) + .flatten() } #[allow(clippy::type_complexity)] diff --git a/src/process/macos.rs b/src/process/macos.rs index e583eb7be..6c8ba3643 100644 --- a/src/process/macos.rs +++ b/src/process/macos.rs @@ -1,85 +1,208 @@ -use libc::{c_int, c_void, size_t}; -use libproc::libproc::bsd_info::BSDInfo; +use crate::process::{ProcessInfoBase, ShowFilter, thread_key}; +use libc::{c_char, c_int, c_void, size_t}; use libproc::libproc::file_info::{ListFDs, ProcFDType, pidfdinfo}; use libproc::libproc::net_info::{InSockInfo, SocketFDInfo, SocketInfoKind, TcpSockInfo}; use libproc::libproc::pid_rusage::{RUsageInfoV2, pidrusage}; -use libproc::libproc::proc_pid::{ListThreads, listpidinfo, pidinfo}; -use libproc::libproc::task_info::{TaskAllInfo, TaskInfo}; +use libproc::libproc::proc_pid::{ListThreads, listpidinfo, pidinfo, pidpath}; +use libproc::libproc::task_info::TaskInfo; use libproc::libproc::thread_info::ThreadInfo; -use libproc::processes::{ProcFilter, pids_by_type}; use mach2::{boolean, vm_types}; +use std::collections::HashMap; use std::ffi::OsStr; use std::path::{Path, PathBuf}; -use std::thread; use std::time::{Duration, Instant}; +use std::{cmp, io, mem, ptr, thread}; pub struct ProcessInfo { - pub pid: i32, - pub ppid: i32, - pub curr_task: TaskAllInfo, - pub prev_task: TaskAllInfo, + /// The part of a row every platform has - the row key, the parent and the + /// sampling window. The `Deref` below hands it out, so a column goes on + /// writing `proc.pid` without reaching for `base`. + /// + /// Threads and processes share the key because every column keys its + /// contents by it. `PROC_PIDLISTTHREADS` hands out 64 bit ids that mean + /// nothing on their own and would land on real pids (or on each other) if + /// they were used as keys directly, so a thread row negates its id: real + /// pids are never negative, and the negation of a thread id is unique to + /// that thread. The Pid column turns it back into the id. + pub base: ProcessInfoBase, + pub curr_proc: kinfo_proc, + // Kept for the same reason freebsd keeps one: the two-sample shape of + // `collect_proc` is shared, even if no column reads the previous one. + #[allow(dead_code)] + pub prev_proc: kinfo_proc, + pub curr_task: TaskInfo, + pub prev_task: TaskInfo, pub curr_path: Option, - pub curr_threads: Vec, pub curr_udps: Vec, pub curr_tcps: Vec, pub curr_res: Option, pub prev_res: Option, - pub interval: Duration, + pub state: i32, +} + +process_info_deref!(); + +/// Map the BSD process-table state (`extern_proc::p_stat`) onto the process +/// state codes the `State` column prints on macOS (see `state.rs`). +/// +/// Only two of the five values the field can hold are worth reading. XNU +/// moved the process state onto the threads long ago and left `p_stat` +/// behind: a snapshot of the whole table (604 processes, macOS 26) has every +/// last one of them at `SRUN`, whether it is running or has been asleep +/// since boot. The field is written again only when the process is stopped +/// (`SSTOP`, confirmed by stopping one) or dies (`SZOMB`, confirmed with an +/// unreaped child); `SSLEEP` and `SIDL` never showed up at all. +/// +/// So `SRUN` says "alive", not "running". Reporting it as "R" would mark +/// every process this fallback covers as busy - worse than the "?" it is +/// meant to improve on - so it is left unknown: +/// +/// | `p_stat` | name | meaning | procs | +/// |----------|--------|-------------------------------|-------| +/// | 1 SIDL | born | being created by fork | 7 "?" | +/// | 2 SRUN | run | alive - held by every process | 7 "?" | +/// | 3 SSLEEP | sleep | never written by XNU | 7 "?" | +/// | 4 SSTOP | stop | stopped / suspended | 5 "T" | +/// | 5 SZOMB | zombie | awaiting collection by parent | 8 "Z" | +/// +/// Used as a fallback when `PROC_PIDLISTTHREADS` is denied (EPERM) and the +/// thread-derived state cannot be computed. +fn state_from_pstat(p_stat: libc::c_char) -> i32 { + match p_stat as i32 { + 4 => 5, // SSTOP -> T + 5 => 8, // SZOMB -> Z + // SIDL, SRUN, SSLEEP and anything else say nothing about what the + // process is doing now. + _ => 7, // unknown + } } pub fn collect_proc( interval: Duration, - _with_thread: bool, - show_kthreads: bool, + with_thread: bool, _procfs_path: &Option, + filter: ShowFilter, ) -> Vec { - let mut base_procs = Vec::new(); + let mut base_procs = HashMap::new(); let mut ret = Vec::new(); let arg_max = get_arg_max(); - if let Ok(procs) = pids_by_type(ProcFilter::All) { - for p in procs { - if let Ok(task) = pidinfo::(p as i32, 0) { - if !show_kthreads && task.pbsd.pbi_flags & 1 /* PROC_FLAG_SYSTEM */ != 0 { - continue; - } + if let Some(procs) = sysctl_procs(filter.userid) { + for kp in procs { + if !filter.kthread && kp.kp_proc.p_flag & P_SYSTEM != 0 { + continue; + } - let res = pidrusage::(p as i32).ok(); - let time = Instant::now(); - base_procs.push((p as i32, task, res, time)); + // The uid comes with the process table, so dropping a process here + // saves the resource usage query and everything the second pass + // does for it. + + let pid = kp.kp_proc.p_pid; + // `pidinfo` is gated by *effective* uid, so it fails with `EPERM` + // for processes owned by other users. Everything else libproc + // offers - `pidrusage`, `PROC_PIDLISTFDS`, `PROC_PIDLISTTHREADS`, + // and the `KERN_PROCARGS2` sysctl - is gated the same way, so a + // `TaskInfo` that failed means the rest of the queries would too: + // the flag lets the second pass skip them all. + let (task, task_info_ok) = match pidinfo::(pid, 0) { + Ok(task) => (task, true), + Err(_) => { + // EPERM or ESRCH + let mut res: TaskInfo = unsafe { mem::zeroed() }; + res.pti_priority = kp.kp_proc.p_priority as i32; // BSD priority + // p_estcpu/p_cpticks/p_pctcpu/p_uticks/p_sticks/p_iticks/p_rtime/p_swtime/p_slptime/p_usrpri + // e_xsize / e_xccount / e_xswrss are all zeros. + (res, false) + } + }; + // `curr_res` stays `None` when `proc_pid_rusage` is denied (EPERM): + // no `kinfo_proc` field maps to `rusage_info_v*`, and a synthesized + // zero-filled struct would make `VmTotal` prefer `ri_phys_footprint + // == 0` over the real `TaskInfo.pti_resident_size`. The columns + // already fall back gracefully on `None`. + let res = if task_info_ok { + pidrusage::(pid).ok() + } else { + None + }; + let time = Instant::now(); + + // The thread list is part of the first sample: the second pass + // pairs each current thread with the one the first pass saw. + let mut threads = Vec::new(); + if with_thread && task_info_ok { + // Over-allocate the buffer to reduce the chance of truncation. + let threadids = listpidinfo::(pid, (task.pti_threadnum as usize) * 2); + if let Ok(threadids) = threadids { + threads.reserve(threadids.len()); + for t in threadids { + // `0` is what the kernel reports for a thread it has + // no id for. It is not a thread to ask about, and it + // would collide with every other one of them. + if t == 0 { + continue; + } + if let Ok(thread) = pidinfo::(pid, t) { + threads.push((t, thread)); + } + } + } } + base_procs.insert(pid, (kp, task, res, time, threads, task_info_ok)); } } thread::sleep(interval); - for (pid, prev_task, prev_res, prev_time) in base_procs { - let curr_task = if let Ok(task) = pidinfo::(pid, 0) { - task + for curr_proc in sysctl_procs(filter.userid).unwrap_or_default() { + // Either the process is gone, or libproc was never willing to talk + // about it: keeping the sample taken before the sleep is what makes + // the deltas come out as zero instead of as a jump. + let pid = curr_proc.kp_proc.p_pid; + let (prev_proc, prev_task, prev_res, prev_time, prev_threads, task_info_ok) = + match base_procs.remove(&pid) { + Some(data) => data, + None => continue, + }; + + let curr_task = if task_info_ok { + pidinfo::(pid, 0).unwrap_or(prev_task) } else { - clone_task_all_info(&prev_task) + prev_task }; - let curr_path = get_path_info(pid, arg_max); + if prev_proc.kp_proc.p_starttime != curr_proc.kp_proc.p_starttime { + // Pid recycled + continue; + } - let threadids = listpidinfo::(pid, curr_task.ptinfo.pti_threadnum as usize); - let mut curr_threads = Vec::new(); - if let Ok(threadids) = threadids { - for t in threadids { - if let Ok(thread) = pidinfo::(pid, t) { - curr_threads.push(thread); - } - } + // The command line, from whichever source is willing to give one: + // `KERN_PROCARGS2` gives all of it, `proc_pidpath` still gives the + // executable for processes it will not, and the process table always + // carries a name. A process whose `TaskInfo` was denied (EPERM) + // skips the `KERN_PROCARGS2` sysctl, but `proc_pidpath` is still + // worth a try before falling back to the process table name. + let curr_path = if task_info_ok { + get_path_info(pid, arg_max).filter(|path| !path.cmd.is_empty()) + } else { + None } + .or_else(|| pidpath(pid).ok().and_then(path_info_of_exe)) + .or_else(|| path_info_of_exe(command_of_comm(&curr_proc.kp_proc.p_comm))); let mut curr_tcps = Vec::new(); let mut curr_udps = Vec::new(); - let fds = listpidinfo::(pid, curr_task.pbsd.pbi_nfiles as usize); - if let Ok(fds) = fds { - for fd in fds { - if let ProcFDType::Socket = fd.proc_fdtype.into() { - if let Ok(socket) = pidfdinfo::(pid, fd.proc_fd) { + // The process table carries no open file count, and the default + // `RLIMIT_NOFILE` soft limit is 256: a buffer that size covers the + // sockets the port columns care about. + if task_info_ok { + let fds = listpidinfo::(pid, 256); + if let Ok(fds) = fds { + for fd in fds { + if let ProcFDType::Socket = fd.proc_fdtype.into() + && let Ok(socket) = pidfdinfo::(pid, fd.proc_fd) + { match socket.psi.soi_kind.into() { SocketInfoKind::In => { if socket.psi.soi_protocol == libc::IPPROTO_UDP { @@ -98,32 +221,251 @@ pub fn collect_proc( } } - let curr_res = pidrusage::(pid).ok(); + let curr_res = if task_info_ok { + pidrusage::(pid).ok() + } else { + None + }; let curr_time = Instant::now(); let interval = curr_time - prev_time; - let ppid = curr_task.pbsd.pbi_ppid as i32; + let ppid = curr_proc.kp_eproc.e_ppid; + let mut state = 7; + + // What a thread row falls back to when the thread itself is unnamed, + // which is most of them: the name of the process it belongs to. + let thread_owner = curr_path + .as_ref() + .filter(|path| !path.name.is_empty()) + .map(|path| path.name.clone()) + .unwrap_or_else(|| command_of_comm(&curr_proc.kp_proc.p_comm)); + + let mut threads = Vec::new(); + + if task_info_ok + && let Ok(threadids) = + listpidinfo::(pid, (curr_task.pti_threadnum as usize) * 2) + { + threads.reserve(threadids.len()); + for tid in threadids { + // See the first pass: `0` is not a thread id. + if tid == 0 { + continue; + } + if let Ok(thread) = pidinfo::(pid, tid) { + let tstate = match thread.pth_run_state { + 1 => 1, // TH_STATE_RUNNING + 2 => 5, // TH_STATE_STOPPED + 3 => { + if thread.pth_sleep_time > 20 { + 4 + } else { + 3 + } + } // TH_STATE_WAITING + 4 => 2, // TH_STATE_UNINTERRUPTIBLE + 5 => 6, // TH_STATE_HALTED + _ => 7, + }; + state = cmp::min(tstate, state); + + if with_thread { + let prev_thread = prev_threads + .iter() + .find(|(ptid, _)| *ptid == tid) + .map(|&(_, t)| t) + .unwrap_or(unsafe { mem::zeroed() }); + + let mut task = unsafe { mem::zeroed::() }; + task.pti_total_user = thread.pth_user_time; + task.pti_total_system = thread.pth_system_time; + task.pti_priority = thread.pth_priority; + task.pti_threadnum = 1; + + let mut prev = unsafe { mem::zeroed::() }; + prev.pti_total_user = prev_thread.pth_user_time; + prev.pti_total_system = prev_thread.pth_system_time; + prev.pti_priority = prev_thread.pth_priority; + prev.pti_threadnum = 1; + + let name = String::from_utf8_lossy( + &thread + .pth_name + .iter() + .take_while(|&&c| c != 0) + .map(|&c| c as u8) + .collect::>(), + ) + .into_owned(); + + let path = PathInfo { + name: if name.is_empty() { + thread_owner.clone() + } else { + name + }, + exe: PathBuf::new(), + root: PathBuf::new(), + cmd: Vec::new(), + env: Vec::new(), + }; + + // Negating the id is what keeps the row key unique: + // a thread can then neither be mistaken for the + // process it belongs to nor take the place of another + // thread. `0` in particular stops being reachable, + // which matters because the tree column reads "a ppid + // that is not in the table" as the root of the tree - + // and 0 is the ppid of launchd. + let key = thread_key(tid); + + // A thread row keeps the process's `kinfo_proc`, + // because that is where its uid, gid, tty and comm + // come from - a thread has no identity of its own + // beyond its id and its name. The start time is the + // one field that belongs to the process alone, and + // leaving it in would date every thread by the process + // it belongs to, so it is blanked here. `StartTime` + // and `ElapsedTime` read a zero timestamp as nothing + // to show - the same blank they print when the time is + // missing altogether. + let mut thread_proc = curr_proc; + thread_proc.kp_proc.p_starttime = libc::timeval { + tv_sec: 0, + tv_usec: 0, + }; + + threads.push(ProcessInfo { + base: ProcessInfoBase::new(key, pid as i64, interval), + curr_proc: thread_proc, + prev_proc, + curr_task: task, + prev_task: prev, + curr_path: Some(path), + curr_udps: Vec::new(), + curr_tcps: Vec::new(), + curr_res: None, + prev_res: None, + state: tstate, + }); + } + } + } + } else { + // `PROC_PIDLISTTHREADS` is gated by *effective* uid, so it fails + // (EPERM) for processes owned by other users - and so does + // `pidinfo` itself, which is why `task_info_ok` is false - and we + // cannot derive a state from their threads. `kinfo_proc` is still + // there for them, but `kp_proc.p_stat` only knows whether the + // process was stopped or has died - see `state_from_pstat` - so + // most rows this covers stay at "?" rather than claiming to run. + state = state_from_pstat(curr_proc.kp_proc.p_stat); + } - let proc = ProcessInfo { - pid, - ppid, + ret.push(ProcessInfo { + base: ProcessInfoBase::new(pid as i64, ppid as i64, interval), + curr_proc, + prev_proc, curr_task, prev_task, curr_path, - curr_threads, curr_udps, curr_tcps, curr_res, prev_res, - interval, - }; - - ret.push(proc); + state, + }); + ret.extend(threads); } ret } +// --------------------------------------------------------------------------- +// The process table +// --------------------------------------------------------------------------- + +/// `KERN_PROC_ALL` from ``: the whole process table. `libc` +/// does not export the `KERN_PROC_*` values for macOS. +const KERN_PROC_ALL: c_int = 0; + +/// `KERN_PROC_UID` from ``: fetch only processes whose +/// effective UID matches the supplied value. `libc` does not export it. +const KERN_PROC_UID: c_int = 5; + +/// `P_SYSTEM` from ``: a process the kernel owns, the ones +/// `ShowFilter::kthread` hides. It lives in the `P_*` namespace of +/// `kinfo_proc::kp_proc::p_flag`. +const P_SYSTEM: c_int = 0x00000200; + +/// Every process on the system, or only the processes of a single user if +/// `uid` is set. +/// +/// `libproc` answers for the processes `procs` owns only: `pidinfo`, +/// `pidrusage` and the rest of it fail with `EPERM` for anything else, which +/// is why a listing built on them alone is a listing of the current user's +/// processes, whatever the `ShowFilter` says. `sysctl` has no such rule and +/// hands out one `kinfo_proc` per process, whoever owns it, with the BSD half +/// of what the columns ask for - uid, ppid, pgid, tty, niceness, start time, +/// name - already in it. +fn sysctl_procs(uid: Option) -> Option> { + let (mib, mib_len): (&[c_int], u32) = match uid { + Some(uid) => ( + &[libc::CTL_KERN, libc::KERN_PROC, KERN_PROC_UID, uid as c_int], + 4, + ), + None => (&[libc::CTL_KERN, libc::KERN_PROC, KERN_PROC_ALL], 3), + }; + let mut length: size_t = 0; + + unsafe { + // Ask how large the table is ... + if libc::sysctl( + mib.as_ptr() as *mut c_int, + mib_len, + ptr::null_mut(), + &mut length, + ptr::null_mut(), + 0, + ) != 0 + { + return None; + } + + // ... and then ask for more than that: the table keeps growing while + // we are reading it, and a buffer that ends up too small gets an + // ENOMEM and no data at all. + length += length / 8 + mem::size_of::(); + + for _ in 0..8 { + let count = length / mem::size_of::(); + let mut procs: Vec = Vec::new(); + procs.resize_with(count, || mem::zeroed()); + + if libc::sysctl( + mib.as_ptr() as *mut c_int, + mib_len, + procs.as_mut_ptr() as *mut c_void, + &mut length, + ptr::null_mut(), + 0, + ) == 0 + { + procs.truncate(length / mem::size_of::()); + return Some(procs); + } + + if io::Error::last_os_error().raw_os_error() != Some(libc::ENOMEM) { + return None; + } + + length += length / 8 + mem::size_of::(); + } + } + + None +} + fn get_arg_max() -> size_t { let mut mib: [c_int; 2] = [libc::CTL_KERN, libc::KERN_ARGMAX]; let mut arg_max = 0i32; @@ -143,27 +485,31 @@ fn get_arg_max() -> size_t { } pub struct PathInfo { - #[allow(dead_code)] pub name: String, #[allow(dead_code)] pub exe: PathBuf, + /// The directory the executable was loaded from #[allow(dead_code)] pub root: PathBuf, pub cmd: Vec, - #[allow(dead_code)] + /// The environment, as `KERN_PROCARGS2` reported it: raw `KEY=VALUE` + /// strings, the ones that follow the arguments in the same buffer. pub env: Vec, } -unsafe fn get_unchecked_str(cp: *mut u8, start: *mut u8) -> String { +/// The bytes between `start` and `cp` as a string. +/// +/// `KERN_PROCARGS2` promises nothing about encoding, and an argument or an +/// environment variable can hold any bytes at all, so invalid UTF-8 is +/// replaced instead of being assumed away. +unsafe fn get_str(cp: *mut u8, start: *mut u8) -> String { let len = cp as usize - start as usize; - let part = Vec::from_raw_parts(start, len, len); - let tmp = String::from_utf8_unchecked(part.clone()); - ::std::mem::forget(part); - tmp + let part = unsafe { std::slice::from_raw_parts(start, len) }; + String::from_utf8_lossy(part).into_owned() } fn get_path_info(pid: i32, mut size: size_t) -> Option { - let mut proc_args = Vec::with_capacity(size as usize); + let mut proc_args = Vec::with_capacity(size); let ptr: *mut u8 = proc_args.as_mut_slice().as_mut_ptr(); let mut mib: [c_int; 3] = [libc::CTL_KERN, libc::KERN_PROCARGS2, pid as c_int]; @@ -190,7 +536,7 @@ fn get_path_info(pid: i32, mut size: size_t) -> Option { while cp < ptr.add(size) && *cp != 0 { cp = cp.offset(1); } - let exe = Path::new(get_unchecked_str(cp, start).as_str()).to_path_buf(); + let exe = Path::new(get_str(cp, start).as_str()).to_path_buf(); let name = exe .file_name() .unwrap_or_else(|| OsStr::new("")) @@ -199,11 +545,11 @@ fn get_path_info(pid: i32, mut size: size_t) -> Option { .to_owned(); let mut need_root = true; let mut root = Default::default(); - if exe.is_absolute() { - if let Some(parent) = exe.parent() { - root = parent.to_path_buf(); - need_root = false; - } + if exe.is_absolute() + && let Some(parent) = exe.parent() + { + root = parent.to_path_buf(); + need_root = false; } while cp < ptr.add(size) && *cp == 0 { cp = cp.offset(1); @@ -214,7 +560,7 @@ fn get_path_info(pid: i32, mut size: size_t) -> Option { while c < n_args && cp < ptr.add(size) { if *cp == 0 { c += 1; - cmd.push(get_unchecked_str(cp, start)); + cmd.push(get_str(cp, start)); start = cp.offset(1); } cp = cp.offset(1); @@ -226,7 +572,7 @@ fn get_path_info(pid: i32, mut size: size_t) -> Option { if cp == start { break; } - env.push(get_unchecked_str(cp, start)); + env.push(get_str(cp, start)); start = cp.offset(1); } cp = cp.offset(1); @@ -256,52 +602,41 @@ fn get_path_info(pid: i32, mut size: size_t) -> Option { } } -fn clone_task_all_info(src: &TaskAllInfo) -> TaskAllInfo { - let pbsd = BSDInfo { - pbi_flags: src.pbsd.pbi_flags, - pbi_status: src.pbsd.pbi_status, - pbi_xstatus: src.pbsd.pbi_xstatus, - pbi_pid: src.pbsd.pbi_pid, - pbi_ppid: src.pbsd.pbi_ppid, - pbi_uid: src.pbsd.pbi_uid, - pbi_gid: src.pbsd.pbi_gid, - pbi_ruid: src.pbsd.pbi_ruid, - pbi_rgid: src.pbsd.pbi_rgid, - pbi_svuid: src.pbsd.pbi_svuid, - pbi_svgid: src.pbsd.pbi_svgid, - rfu_1: src.pbsd.rfu_1, - pbi_comm: src.pbsd.pbi_comm, - pbi_name: src.pbsd.pbi_name, - pbi_nfiles: src.pbsd.pbi_nfiles, - pbi_pgid: src.pbsd.pbi_pgid, - pbi_pjobc: src.pbsd.pbi_pjobc, - e_tdev: src.pbsd.e_tdev, - e_tpgid: src.pbsd.e_tpgid, - pbi_nice: src.pbsd.pbi_nice, - pbi_start_tvsec: src.pbsd.pbi_start_tvsec, - pbi_start_tvusec: src.pbsd.pbi_start_tvusec, - }; - let ptinfo = TaskInfo { - pti_virtual_size: src.ptinfo.pti_virtual_size, - pti_resident_size: src.ptinfo.pti_resident_size, - pti_total_user: src.ptinfo.pti_total_user, - pti_total_system: src.ptinfo.pti_total_system, - pti_threads_user: src.ptinfo.pti_threads_user, - pti_threads_system: src.ptinfo.pti_threads_system, - pti_policy: src.ptinfo.pti_policy, - pti_faults: src.ptinfo.pti_faults, - pti_pageins: src.ptinfo.pti_pageins, - pti_cow_faults: src.ptinfo.pti_cow_faults, - pti_messages_sent: src.ptinfo.pti_messages_sent, - pti_messages_received: src.ptinfo.pti_messages_received, - pti_syscalls_mach: src.ptinfo.pti_syscalls_mach, - pti_syscalls_unix: src.ptinfo.pti_syscalls_unix, - pti_csw: src.ptinfo.pti_csw, - pti_threadnum: src.ptinfo.pti_threadnum, - pti_numrunning: src.ptinfo.pti_numrunning, - pti_priority: src.ptinfo.pti_priority, - }; - TaskAllInfo { pbsd, ptinfo } +/// A `PathInfo` for a process whose command line nobody will tell us: `exe` +/// is both the executable and the whole command line. +fn path_info_of_exe(exe: String) -> Option { + if exe.is_empty() { + return None; + } + + let path = PathBuf::from(&exe); + let name = path + .file_name() + .unwrap_or_else(|| OsStr::new("")) + .to_str() + .unwrap_or("") + .to_owned(); + let root = path.parent().unwrap_or_else(|| Path::new("")).to_path_buf(); + + Some(PathInfo { + exe: path, + name, + root, + cmd: vec![exe], + env: Vec::new(), + }) +} + +/// The name carried by `comm`, a `p_comm` the kernel NUL pads and truncates +/// to `MAXCOMLEN` characters. +fn command_of_comm(comm: &[c_char]) -> String { + let bytes = comm + .iter() + .take_while(|&&c| c != 0) + .map(|&c| c as u8) + .collect::>(); + + String::from_utf8_lossy(&bytes).into_owned() } // https://github.com/rust-psutil/rust-psutil/blob/main/src/process/os/macos/kinfo.rs @@ -339,24 +674,16 @@ pub struct kinfo_proc { pub kp_eproc: kinfo_proc_eproc, } -#[repr(C)] -#[derive(Copy, Clone)] -pub struct run_sleep_queue { - p_forw: vm_types::user_addr_t, - p_back: vm_types::user_addr_t, -} - -#[repr(C)] -#[derive(Copy, Clone)] -pub union p_un { - pub p_st1: run_sleep_queue, - pub p_starttime: libc::timeval, -} - #[repr(C)] #[derive(Copy, Clone)] pub struct extern_proc { - pub p_un: p_un, + /// The process start time. + /// + /// XNU stores it in a union with the run/sleep queue links (`p_st1`), + /// and the two members are the same size and alignment on every + /// architecture macOS runs on, so reading it as a plain `timeval` keeps + /// the layout identical to the kernel's `extern_proc`. + pub p_starttime: libc::timeval, pub p_vmspace: vm_types::user_addr_t, pub p_sigacts: vm_types::user_addr_t, @@ -431,3 +758,18 @@ pub struct kinfo_proc_eproc { pub e_login: [libc::c_char; 12], pub e_spare: [i32; 4], } + +#[cfg(test)] +mod tests { + use super::*; + + /// `KERN_PROCARGS2` hands the environment back along with the arguments, + /// which is what the Env column reads. + #[test] + fn path_info_carries_the_environment() { + let info = get_path_info(std::process::id() as i32, get_arg_max()).unwrap(); + assert!(!info.cmd.is_empty()); + assert!(!info.env.is_empty()); + assert!(info.env.iter().all(|entry| !entry.is_empty())); + } +} diff --git a/src/process/ntapi.rs b/src/process/ntapi.rs index 0043632e7..c1ceb29a5 100644 --- a/src/process/ntapi.rs +++ b/src/process/ntapi.rs @@ -16,13 +16,13 @@ use windows_sys::Wdk::System::SystemInformation::{ }; use windows_sys::Wdk::System::SystemServices::VM_COUNTERS; use windows_sys::Wdk::System::Threading::{ - NtQueryInformationProcess, ProcessBasicInformation, ProcessCommandLineInformation, - ProcessImageInformation, + NtQueryInformationProcess, NtQueryInformationThread, ProcessBasicInformation, + ProcessCommandLineInformation, ProcessImageInformation, ThreadNameInformation, }; use windows_sys::Win32::Foundation::UNICODE_STRING; use windows_sys::Win32::Foundation::{HANDLE, STATUS_INFO_LENGTH_MISMATCH, STATUS_SUCCESS}; use windows_sys::Win32::Security::{PSID, SECURITY_MAX_SID_SIZE, SID}; -use windows_sys::Win32::System::Threading::{IO_COUNTERS, PROCESS_BASIC_INFORMATION}; +use windows_sys::Win32::System::Threading::{IO_COUNTERS, PEB, PROCESS_BASIC_INFORMATION}; use windows_sys::Win32::System::WindowsProgramming::CLIENT_ID; // --------------------------------------------------------------------------- @@ -219,29 +219,10 @@ const _: () = assert!(size_of::() == 48); #[cfg(target_pointer_width = "32")] const _: () = assert!(offset_of!(SECTION_IMAGE_INFORMATION, Machine) == 32); -/// The leading, version-stable part of `PEB`, up to and including -/// `ProcessParameters`. -/// -/// Everything after `ProcessParameters` has grown and been reordered across -/// Windows versions, but the prefix has stayed put since XP, which is all the -/// working-directory read needs. -#[repr(C)] -#[allow(non_snake_case)] -pub struct PEB_PREFIX { - pub InheritedAddressSpace: u8, - pub ReadImageFileExecOptions: u8, - pub BeingDebugged: u8, - pub BitField: u8, - pub Mutant: HANDLE, - pub ImageBaseAddress: *mut c_void, - pub Ldr: *mut c_void, - pub ProcessParameters: *mut c_void, -} - #[cfg(target_pointer_width = "64")] -const _: () = assert!(offset_of!(PEB_PREFIX, ProcessParameters) == 0x20); +const _: () = assert!(offset_of!(PEB, ProcessParameters) == 0x20); #[cfg(target_pointer_width = "32")] -const _: () = assert!(offset_of!(PEB_PREFIX, ProcessParameters) == 0x10); +const _: () = assert!(offset_of!(PEB, ProcessParameters) == 0x10); /// `CURDIR` - the current-directory record of /// `RTL_USER_PROCESS_PARAMETERS`. @@ -450,6 +431,62 @@ impl std::fmt::Display for SID_MAX { } } +impl std::str::FromStr for SID_MAX { + type Err = (); + + /// The SID as Windows writes it: `S-1-5-21-...-1001`. + /// + /// Only the parts that are compared are read - the revision, the + /// identifier authority and the sub-authorities - so a SID that went + /// through `Display` parses back to an equal one. Anything else is `Err`, + /// including a SID with more sub-authorities than a SID can hold, so that + /// a value which only looks like a SID is left to be read as something + /// else. + fn from_str(s: &str) -> Result { + let rest = s.strip_prefix("S-").ok_or(())?; + let mut parts = rest.split('-'); + + let revision: u8 = parts.next().ok_or(())?.parse().map_err(|_| ())?; + let authority: u64 = parts.next().ok_or(())?.parse().map_err(|_| ())?; + // The identifier authority is 48 bits wide. + if authority > 0xffff_ffff_ffff { + return Err(()); + } + + let mut subs = [0u32; SID_MAX_SUB_AUTHORITIES]; + let mut count = 0; + for part in parts { + if count == SID_MAX_SUB_AUTHORITIES { + return Err(()); + } + subs[count] = part.parse().map_err(|_| ())?; + count += 1; + } + if count == 0 { + return Err(()); + } + + // Zeroed first so that the padding is never read as part of the SID: + // equality and hashing go over the live bytes only. + let mut ret: SID_MAX = unsafe { std::mem::zeroed() }; + ret.sid.Revision = revision; + ret.sid.SubAuthorityCount = count as u8; + ret.sid.IdentifierAuthority.Value = authority.to_be_bytes()[2..].try_into().unwrap(); + // SAFETY: `subs` holds at most `SID_MAX_SUB_AUTHORITIES` entries, the + // count `SubAuthorityCount` is set from, and the sub-authorities are + // contiguous from `SubAuthority[0]` in the buffer, which is + // `SECURITY_MAX_SID_SIZE` bytes - room for 15 of them. + unsafe { + ptr::copy_nonoverlapping( + subs.as_ptr(), + ptr::addr_of_mut!(ret.sid.SubAuthority) as *mut u32, + count, + ); + } + Ok(ret) + } +} + impl PartialEq for SID_MAX { fn eq(&self, other: &Self) -> bool { self.as_bytes() == other.as_bytes() @@ -536,6 +573,15 @@ impl SystemProcessSnapshot { pub fn iter(&self) -> ProcessIter<'_> { ProcessIter::new(self) } + + /// Whether the entries of this snapshot carry a user SID. + /// + /// Only class 148 does. When it does, the user filter can drop a process + /// without opening a handle for it; with the basic class the SID is only + /// available from the process token, so the filter has to wait for that. + pub fn carries_user_sid(&self) -> bool { + self.kind.has_extension() + } } /// Takes one process snapshot. @@ -677,7 +723,10 @@ fn activity(state: ThreadState) -> u8 { /// for it. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ThreadSnapshot { - pub tid: i32, + /// Thread id, wide enough for the whole `HANDLE`-sized value the kernel + /// reports. Callers turn it into a row key with + /// [`crate::process::thread_key`], which negates it. + pub tid: u64, /// Owning process id. pub pid: i32, pub create_time: i64, @@ -758,7 +807,7 @@ impl<'a> ProcessEntry<'a> { }; ret.push(ThreadSnapshot { - tid: thread.ClientId.UniqueThread as usize as i32, + tid: thread.ClientId.UniqueThread as usize as u64, pid, create_time: thread.CreateTime, kernel_time: thread.KernelTime as u64, @@ -1084,6 +1133,94 @@ pub fn process_command_line(handle: HANDLE) -> Option { ) } +// --------------------------------------------------------------------------- +// ThreadNameInformation +// --------------------------------------------------------------------------- + +/// `THREAD_NAME_INFORMATION` - the output of `ThreadNameInformation` (38). +/// +/// `ThreadName` describes characters the kernel copies into the space behind +/// this structure, which is why the buffer handed to the query is always +/// larger than the structure itself. +#[repr(C)] +#[allow(non_snake_case)] +pub struct THREAD_NAME_INFORMATION { + pub ThreadName: UNICODE_STRING, +} + +/// Guard against a nonsensical `UNICODE_STRING::Length` turning into a huge +/// allocation. `SetThreadDescription` caps the name far below this. +const MAX_THREAD_NAME_BYTES: usize = 64 * 1024; + +/// Reads the name the thread behind `handle` carries, if any. +/// +/// `ThreadNameInformation` is a Windows 10 addition: it reports the name a +/// process gave the thread through `SetThreadDescription`, and nothing at all +/// for the many threads nobody named. It needs +/// `THREAD_QUERY_LIMITED_INFORMATION` on the handle. An older build, a thread +/// without a name, and a thread that refuses the query all yield `None` - +/// the caller falls back to the name of the process the thread belongs to. +pub fn thread_name(handle: HANDLE) -> Option { + // Room for the header plus a typical name. + let mut buf = + vec![0u64; (size_of::() + 256).div_ceil(size_of::())]; + let mut status = STATUS_INFO_LENGTH_MISMATCH; + + for _ in 0..4 { + let mut ret_len: u32 = 0; + status = unsafe { + NtQueryInformationThread( + handle, + ThreadNameInformation, + buf.as_mut_ptr().cast::(), + (buf.len() * size_of::()) as u32, + ptr::addr_of_mut!(ret_len), + ) + }; + if status != STATUS_INFO_LENGTH_MISMATCH { + break; + } + // A thread without a name answers `STATUS_SUCCESS` with a length of + // 0 here, so grow on our own too rather than asking for nothing. + let want = (ret_len as usize).max(buf.len() * size_of::() * 2); + if want > MAX_THREAD_NAME_BYTES { + return None; + } + buf.resize(want.div_ceil(size_of::()), 0); + } + if status < 0 { + return None; + } + + // SAFETY: the buffer is 8-byte aligned and the query reported success. + let info: &THREAD_NAME_INFORMATION = + unsafe { &*buf.as_ptr().cast::() }; + if info.ThreadName.Buffer.is_null() || info.ThreadName.Length == 0 { + return None; + } + let bytes = info.ThreadName.Length as usize; + if bytes > MAX_THREAD_NAME_BYTES { + return None; + } + + let start = buf.as_ptr() as usize; + let end = start + buf.len() * size_of::(); + let addr = info.ThreadName.Buffer as usize; + let last = addr.checked_add(bytes)?; + if addr < start || last > end { + return None; + } + + // SAFETY: the range was just verified to lie inside `buf`, which is still + // alive. + let chars = unsafe { std::slice::from_raw_parts(info.ThreadName.Buffer, bytes / 2) }; + + let name = String::from_utf16_lossy(chars) + .trim_end_matches('\0') + .to_owned(); + if name.is_empty() { None } else { Some(name) } +} + /// Reads the `IMAGE_FILE_MACHINE_*` of the executable image of `handle`. /// /// `ProcessImageInformation` only needs `PROCESS_QUERY_LIMITED_INFORMATION`, @@ -1706,4 +1843,44 @@ mod tests { assert!(process_network_counters(handle).is_some()); } } + + /// The string a SID is written as parses back to the same SID, and a + /// string that only looks like one does not parse at all - it is then left + /// to be read as a user name. + #[test] + fn sid_parses_its_own_string() { + let text = "S-1-5-21-1111111111-2222222222-3333333333-4444"; + let sid: SID_MAX = text.parse().expect("a SID"); + assert_eq!(sid.format(false), text); + assert_eq!(sid.authority(), 5); + assert_eq!( + sid.sub_authorities(), + [21, 1111111111, 2222222222, 3333333333, 4444] + ); + + // The well known ones, which are the same on every Windows and are + // shorter than the machine SIDs. + let system: SID_MAX = "S-1-5-18".parse().expect("a SID"); + assert_eq!(system.format(false), "S-1-5-18"); + assert_eq!(system.sub_authorities(), [18]); + + // An authority of exactly the 48 bits it is given. + let wide: SID_MAX = "S-1-281474976710655-1".parse().expect("a SID"); + assert_eq!(wide.authority(), 281474976710655); + assert_eq!(wide.format(false), "S-1-281474976710655-1"); + + // Not SIDs: no prefix, no sub-authority, a negative one, one too many, + // and an authority wider than the 48 bits it is given. + for bad in [ + "1-5-18", + "S-1", + "S-1-5", + "S-1-5--1", + "S-1-5-21-1-2-3-4-5-6-7-8-9-10-11-12-13-14-15-16", + "S-1-281474976710656-18", + "S-x-5-18", + ] { + assert!(bad.parse::().is_err(), "{bad} is not a SID"); + } + } } diff --git a/src/process/windows.rs b/src/process/windows.rs index 56786ecdf..180be06c2 100644 --- a/src/process/windows.rs +++ b/src/process/windows.rs @@ -1,3 +1,4 @@ +use anyhow::{Context, Error, anyhow}; use chrono::offset::TimeZone; use chrono::{Local, NaiveDate}; use std::cell::RefCell; @@ -6,25 +7,29 @@ use std::ffi::c_void; use std::mem::{MaybeUninit, zeroed}; use std::path::PathBuf; use std::ptr; +use std::str::FromStr; +use std::sync::OnceLock; use std::sync::atomic::{AtomicU8, Ordering}; use std::thread; use std::time::{Duration, Instant}; use windows_sys::Win32::Foundation::{CloseHandle, FALSE, HANDLE}; use windows_sys::Win32::Security::{ - AdjustTokenPrivileges, GetTokenInformation, LookupAccountSidW, LookupPrivilegeValueW, PSID, - SE_DEBUG_NAME, SE_PRIVILEGE_ENABLED, SID, TOKEN_ADJUST_PRIVILEGES, TOKEN_INFORMATION_CLASS, - TOKEN_PRIVILEGES, TOKEN_QUERY, TOKEN_USER, TokenUser, + AdjustTokenPrivileges, GetTokenInformation, LookupAccountNameW, LookupAccountSidW, + LookupPrivilegeValueW, PSID, SE_DEBUG_NAME, SE_PRIVILEGE_ENABLED, SECURITY_MAX_SID_SIZE, SID, + TOKEN_ADJUST_PRIVILEGES, TOKEN_INFORMATION_CLASS, TOKEN_PRIVILEGES, TOKEN_QUERY, TOKEN_USER, + TokenUser, }; use windows_sys::Win32::System::Threading::{ - GetCurrentProcess, OpenProcess, OpenProcessToken, PROCESS_QUERY_INFORMATION, - PROCESS_QUERY_LIMITED_INFORMATION, + GetCurrentProcess, OpenProcess, OpenProcessToken, OpenThread, PROCESS_QUERY_INFORMATION, + PROCESS_QUERY_LIMITED_INFORMATION, THREAD_QUERY_LIMITED_INFORMATION, }; +use super::ProcessInfoBase; +use super::ShowFilter; use super::ntapi; +use crate::config::ConfigUserFilter; +use crate::process::thread_key; -/// Re-export the PEB prefix so the WorkDir column can read -/// `ProcessParameters`. -pub use super::ntapi::PEB_PREFIX; /// Re-export the process-parameters prefix so the WorkDir column can read /// `CurrentDirectory`. pub use super::ntapi::RTL_USER_PROCESS_PARAMETERS_PREFIX; @@ -53,14 +58,17 @@ pub use super::ntapi::thread_state; pub use super::ntapi::wait_reason; pub struct ProcessInfo { - pub pid: i32, + /// The part of a row every platform has - the row key, the parent and the + /// sampling window. The `Deref` below hands it out, so a column goes on + /// writing `proc.pid` without reaching for `base`. + pub base: ProcessInfoBase, /// Command line of the process, or `None` when the process exposes none /// (e.g. System, Idle). Falls back to `file_name` at display time. pub command: Option, /// Image (executable) name, used as the Command fallback and by the - /// FileName column. + /// FileName column. For a `--thread` row it is the thread's own name when + /// it has one, and the image name of the process it belongs to otherwise. pub file_name: String, - pub ppid: i32, pub start_time: chrono::DateTime, pub cpu_info: CpuInfo, pub memory_info: MemoryInfo, @@ -76,9 +84,10 @@ pub struct ProcessInfo { /// its threads, so the State column derives the row's state from the most /// active one. For a `--thread` row it is that one thread's state. pub state: Option, - pub interval: Duration, } +process_info_deref!(); + pub struct MemoryInfo { pub page_fault_count: u64, pub peak_working_set_size: u64, @@ -142,16 +151,16 @@ pub struct CpuInfo { pub fn collect_proc( interval: Duration, with_thread: bool, - show_kthreads: bool, _procfs_path: &Option, + filter: ShowFilter, ) -> Vec { let _ = set_privilege(); let started = Instant::now(); - let prev = take_snapshot(with_thread); + let prev = take_snapshot(with_thread, filter); thread::sleep(interval); let finished = Instant::now(); - let curr = take_snapshot(with_thread); + let curr = take_snapshot(with_thread, filter); // Several columns divide by this, so never hand out a zero interval. let interval = finished @@ -159,7 +168,7 @@ pub fn collect_proc( .max(Duration::from_millis(1)); let prev_procs: HashMap = prev.procs.iter().map(|p| (p.pid, p)).collect(); - let prev_threads: HashMap = + let prev_threads: HashMap = prev.threads.iter().map(|t| (t.tid, t)).collect(); let SystemSnapshot { procs, threads } = curr; @@ -171,22 +180,40 @@ pub fn collect_proc( // Compression, ...) unless `--thread` was given. The decision is made // once in `take_snapshot` from the authoritative kernel classification // (or the parent-pid heuristic on a basic snapshot). - if !show_kthreads && proc.is_kthread { + if !filter.kthread && proc.is_kthread { continue; } - let prev = prev_procs + let Some(prev) = prev_procs .get(&proc.pid) .copied() - // A recycled pid would otherwise pair up with an unrelated process. - .filter(|p| p.create_time == proc.create_time || proc.create_time == 0); - - // A process that started between the two samples pairs with itself, - // which reports no delta. - let prev = prev.unwrap_or(&proc); + .filter(|p| p.create_time == proc.create_time || proc.create_time == 0) + else { + // Pid recycled + continue; + }; let handles = ProcHandles::open(proc.pid); + // The owner comes first. With a full snapshot it is already in hand and + // `take_snapshot` has filtered on it, so anything that gets this far + // matches; with the basic snapshot the token has to be opened anyway, + // which is what this call does, and dropping the process here at least + // saves reading its command line. The group list is deliberately not + // collected here - `Group` and `Gid` query it themselves, so that only + // enabling one of them pays for it. + let user = proc.user_sid.or_else(|| handles.full.and_then(get_user)); + // A process whose *own* owner cannot be read counts as not the wanted + // user and is dropped: there is no SID to compare, and keeping it + // would let processes of every user through. A filter that is not set + // at all - our own SID not having been readable - is the one case that + // keeps everything, so that the listing is not left empty. + if let Some(userid) = filter.userid + && user != Some(userid) + { + continue; + } + let command = handles .any() .and_then(ntapi::process_command_line) @@ -197,18 +224,12 @@ pub fn collect_proc( // have no command line (e.g. System, Idle). let file_name = image_fallback(&proc); - // The snapshot SID saves an `OpenProcessToken`; the token is only - // opened for processes the snapshot could not name. The group list is - // deliberately not collected here - `Group` and `Gid` query it - // themselves, so that only enabling one of them pays for it. - let user = proc.user_sid.or_else(|| handles.full.and_then(get_user)); let priority = proc.base_priority; ret.push(ProcessInfo { - pid: proc.pid, + base: ProcessInfoBase::new(proc.pid as i64, proc.ppid as i64, interval), command, file_name, - ppid: proc.ppid, start_time: filetime_to_local(proc.create_time), cpu_info: CpuInfo { prev_sys: prev.kernel_time, @@ -235,12 +256,11 @@ pub fn collect_proc( thread: proc.thread_count, session: proc.session_id as i32, state: proc.state, - interval, }); } if with_thread { - let owners: HashMap = ret + let owners: HashMap = ret .iter() .enumerate() .map(|(idx, p)| (p.pid, idx)) @@ -248,7 +268,7 @@ pub fn collect_proc( for thread in threads { // Skip threads whose owning process was filtered out above. - let Some(&owner) = owners.get(&thread.pid) else { + let Some(&owner) = owners.get(&(thread.pid as i64)) else { continue; }; @@ -267,18 +287,21 @@ pub fn collect_proc( let (command, file_name, user, session) = { let parent = &ret[owner]; ( - parent.command.clone(), - parent.file_name.clone(), + None, + // A thread has no image of its own, so the name it is + // listed under is its own when it carries one - which is + // how the Command and FileName columns tell the threads + // of one process apart - and its process' otherwise. + thread_name(thread.tid).unwrap_or_else(|| parent.file_name.clone()), parent.user, parent.session, ) }; ret.push(ProcessInfo { - pid: thread.tid, + base: ProcessInfoBase::new(thread_key(thread.tid), thread.pid as i64, interval), command, file_name, - ppid: thread.pid, start_time: filetime_to_local(thread.create_time), cpu_info: CpuInfo { prev_sys, @@ -306,7 +329,6 @@ pub fn collect_proc( thread: 1, session, state: Some(thread.state), - interval, }); } } @@ -314,6 +336,106 @@ pub fn collect_proc( ret } +// --------------------------------------------------------------------------- +// Current user +// --------------------------------------------------------------------------- + +/// The SID of the user `procs` runs as. +static CURRENT_USER: OnceLock> = OnceLock::new(); + +/// The SID of the user `procs` runs as, or `None` when it cannot be resolved. +/// +/// Reading our own token needs no privilege, so a failure is not expected; a +/// failure yields `None`, which then matches nothing. +fn current_user_sid() -> Option { + *CURRENT_USER.get_or_init(|| token_user(CURRENT_PROCESS_TOKEN)) +} + +/// The user a filter keeps, looked up from what the configuration names. +/// +/// Windows has no uid, so what the filter compares is the whole SID. The two +/// keywords never reach this function - the configuration layer turns them +/// into `All` and `Myself` - and what is left is a user, tried as the SID it +/// is written as and then as the name of an account. A number is looked up as +/// a name like anything else, and so is almost never a user: there is nothing +/// on Windows for a uid to mean. +/// +/// A name that resolves to nothing is a typo, and a listing filtered by a typo +/// comes out empty rather than wrong - empty with a reason beats empty without +/// one. +pub fn user_id_of(user: &ConfigUserFilter) -> Result, Error> { + Ok(match user { + ConfigUserFilter::All => None, + ConfigUserFilter::Myself => Some( + current_user_sid().context("the SID of the user procs runs as could not be read")?, + ), + ConfigUserFilter::User(user) => { + let sid = SID_MAX::from_str(user) + .ok() + .or_else(|| lookup_sid(user)) + .ok_or_else(|| anyhow!("no such user: {user}"))?; + Some(sid) + } + }) +} + +/// The SID of the account with this name, or `None` when there is no such +/// account. +/// +/// The name is resolved the way Windows resolves it: a bare name is looked for +/// among the machine's own accounts and then in the domain, while +/// `DOMAIN\name` - or `MACHINE\name` - says which of them to look in. +fn lookup_sid(name: &str) -> Option { + let name = to_wide(name); + let mut sid_len = 0; + let mut domain_len = 0; + let mut kind = 0; + unsafe { + // The first call only measures, and is what says whether the name is + // one at all: with no buffers it fills in the two lengths, or leaves + // them at zero. + let _ = LookupAccountNameW( + ptr::null(), + name.as_ptr(), + ptr::null_mut(), + &mut sid_len, + ptr::null_mut(), + &mut domain_len, + &mut kind, + ); + + // A SID never exceeds `SECURITY_MAX_SID_SIZE`; a longer one would not + // fit in the `SID_MAX` the second call fills. + if sid_len == 0 || sid_len > SECURITY_MAX_SID_SIZE { + return None; + } + + let mut sid: SID_MAX = zeroed(); + let mut domain = vec![0u16; domain_len as usize]; + let ret = LookupAccountNameW( + ptr::null(), + name.as_ptr(), + &mut sid.sid as *mut SID as PSID, + &mut sid_len, + domain.as_mut_ptr(), + &mut domain_len, + &mut kind, + ); + + (ret != 0).then_some(sid) + } +} + +/// A string as a NUL terminated wide string, for the `*W` APIs. +fn to_wide(s: &str) -> Vec { + use std::os::windows::ffi::OsStrExt; + + std::ffi::OsStr::new(s) + .encode_wide() + .chain(std::iter::once(0)) + .collect() +} + // --------------------------------------------------------------------------- // Snapshot // --------------------------------------------------------------------------- @@ -357,7 +479,7 @@ struct SystemSnapshot { /// /// `SystemFullProcessInformation` is used when it is available, which also /// hands over each process' user SID. -fn take_snapshot(with_thread: bool) -> SystemSnapshot { +fn take_snapshot(with_thread: bool, filter: ShowFilter) -> SystemSnapshot { let mut procs = Vec::new(); let mut threads = Vec::new(); @@ -365,6 +487,12 @@ fn take_snapshot(with_thread: bool) -> SystemSnapshot { return SystemSnapshot { procs, threads }; }; + // One entry of the buffer is `procs` itself, and it carries the SID the + // filter compares against: reading it here, while the buffer is still the + // only thing in hand, is what lets the filter drop a process before a + // single handle is opened for it. + let carries_sid = buffer.carries_user_sid(); + // `ProcessNetworkIoInformation` is a Windows 11 addition: on an older // build no handle is opened for it at all. let want_net = network_counters_supported(); @@ -373,6 +501,22 @@ fn take_snapshot(with_thread: bool) -> SystemSnapshot { let info = entry.info(); let pid = info.UniqueProcessId as usize as i32; let ppid = info.InheritedFromUniqueProcessId as usize as i32; + + // Dropping a process here is free and saves everything the rest of the + // snapshot does for it: the handle for the network counters, a place in + // the process list, and later on the handle and the command line read + // in `collect_proc`. + let user = if carries_sid { entry.user_sid() } else { None }; + // An owner that is not published yet - the class does not carry SIDs, + // or this entry has none - is judged again in `collect_proc`, once its + // token has been read. + if let Some(userid) = filter.userid + && user.is_some() + && user != Some(userid) + { + continue; + } + let image_name = entry.image_name(); // Unlike the rest of the snapshot, the network counters are read from @@ -425,11 +569,17 @@ fn take_snapshot(with_thread: bool) -> SystemSnapshot { }, base_priority: info.BasePriority, state: entry.state(), - user_sid: entry.user_sid(), + user_sid: user, is_kthread, }); - if with_thread { + // Idle (pid 0) is a stand-in the kernel keeps for the idle loop, not a + // process with threads of its own: the records it carries have no + // usable thread id - most of them report none at all, and one that + // reports `0` would collapse onto Idle's own row, since a thread row + // is keyed by its negated id and `-0` is `0` again. So it is listed as + // one childless process. + if with_thread && pid != 0 { threads.extend(entry.threads()); } } @@ -550,11 +700,50 @@ fn open_process(pid: i32, access: u32) -> Option { if handle.is_null() { None } else { Some(handle) } } +// --------------------------------------------------------------------------- +// Thread handles +// --------------------------------------------------------------------------- + +/// The name the thread `tid` carries, if any. +/// +/// Unlike everything else a row shows, the name is not part of the snapshot: +/// it costs an `OpenThread` and a query of its own, so it is paid for only +/// by the threads `--thread` actually lists. `None` when the thread cannot be +/// opened - it may have exited since the snapshot was taken - or when it was +/// never given a name. +fn thread_name(tid: u64) -> Option { + // Thread ids are 32 bit; a wider one is not one the kernel handed out. + let tid = u32::try_from(tid).ok()?; + let handle = unsafe { OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, tid) }; + if handle.is_null() { + return None; + } + + let name = ntapi::thread_name(handle); + unsafe { + CloseHandle(handle); + } + name +} + // --------------------------------------------------------------------------- // Privilege / token // --------------------------------------------------------------------------- +/// The primary token of the calling process. +/// +/// `NtCurrentProcessToken()` of `winnt.h`: a pseudo-handle the kernel +/// resolves to the caller's own token. It needs no `OpenProcessToken` and must +/// never be closed, so nothing here calls `CloseHandle` on it. +/// +/// It is only good for **querying** the token (`GetTokenInformation`). +/// Adjusting privileges through it does not take effect - `set_privilege` has +/// to open a real handle for that. +const CURRENT_PROCESS_TOKEN: HANDLE = -4isize as *mut c_void; + fn set_privilege() -> bool { + // The pseudo-handle cannot be used here: adjusting privileges needs a real + // handle opened with `TOKEN_ADJUST_PRIVILEGES`. let handle = unsafe { GetCurrentProcess() }; let mut token: HANDLE = unsafe { zeroed() }; let ret = unsafe { OpenProcessToken(handle, TOKEN_ADJUST_PRIVILEGES, &mut token) }; @@ -593,6 +782,7 @@ fn set_privilege() -> bool { ret != 0 } +/// The SID of the user the process `handle` runs as. fn get_user(handle: HANDLE) -> Option { let mut token: HANDLE = unsafe { zeroed() }; let ret = unsafe { OpenProcessToken(handle, TOKEN_QUERY, &mut token) }; @@ -600,14 +790,21 @@ fn get_user(handle: HANDLE) -> Option { return None; } - let sid = token_information(token, TokenUser); + let sid = token_user(token); unsafe { CloseHandle(token); } + sid +} +/// The SID of the user `token` belongs to. +/// +/// `token` may be a real handle or the `CURRENT_PROCESS_TOKEN` pseudo-handle; +/// neither is closed here, so the caller keeps owning it. +fn token_user(token: HANDLE) -> Option { // The SID pointer lives inside this buffer, so it has to stay alive for // as long as `psid` is used. - let buf = sid?; + let buf = token_information(token, TokenUser)?; #[allow(clippy::cast_ptr_alignment)] let token_user = buf.as_ptr() as *const TOKEN_USER; @@ -741,3 +938,21 @@ fn from_wide_ptr(ptr: *const u16) -> String { let slice = unsafe { std::slice::from_raw_parts(ptr, len) }; OsString::from_wide(slice).to_string_lossy().into_owned() } + +#[cfg(test)] +mod tests { + use super::*; + + /// The pseudo-handle has to stand for the same token that + /// `OpenProcessToken` hands out for the current process. + #[test] + fn current_process_token_matches_open_process_token() { + let via_pseudo = token_user(CURRENT_PROCESS_TOKEN); + // SAFETY: `GetCurrentProcess` returns a pseudo-handle that must not be + // closed, and `get_user` closes only the token it opens itself. + let via_open = unsafe { get_user(GetCurrentProcess()) }; + + assert!(via_pseudo.is_some()); + assert!(via_pseudo == via_open); + } +} diff --git a/src/term_info.rs b/src/term_info.rs index d38ae6081..614d9604d 100644 --- a/src/term_info.rs +++ b/src/term_info.rs @@ -4,10 +4,15 @@ use minus::Pager; use minus::input::{self, HashedEventRegister, InputEvent}; use std::cell::RefCell; use std::fmt::Write; +use std::io::Write as _; +use std::process::Child; pub struct TermInfo { term: Term, pub pager: RefCell>, + /// External pager process (Windows only, driven by `command` of `[pager]` section). + /// While it is alive, `write_line` feeds the process's stdin instead of the terminal. + pub external_pager: RefCell>, pub height: usize, pub width: usize, pub clear_by_line: bool, @@ -18,6 +23,7 @@ impl TermInfo { pub fn new(clear_by_line: bool, use_pager: bool) -> Result { let term = Term::stdout(); let pager = RefCell::new(Some(gen_pager()?)); + let external_pager = RefCell::new(None); let (term_h, term_w) = term.size(); let height = term_h as usize; let width = term_w as usize; @@ -25,6 +31,7 @@ impl TermInfo { Ok(TermInfo { term, pager, + external_pager, height, width, clear_by_line, @@ -39,11 +46,29 @@ impl TermInfo { if self.use_pager { writeln!(self.pager.borrow_mut().as_mut().unwrap(), "{s}")?; } else { - self.term.write_line(s)?; + let mut external_pager = self.external_pager.borrow_mut(); + match external_pager.as_mut().and_then(|x| x.stdin.as_mut()) { + Some(stdin) => { + stdin.write_all(s.as_bytes())?; + stdin.write_all(b"\n")?; + } + None => self.term.write_line(s)?, + } } Ok(()) } + /// Closes the pipe to the external pager and waits until it exits. + /// Does nothing if no external pager was started. + pub fn finish_external_pager(&self) -> Result<(), Error> { + let Some(mut child) = self.external_pager.replace(None) else { + return Ok(()); + }; + drop(child.stdin.take()); + child.wait()?; + Ok(()) + } + pub fn clear_screen(&self) -> Result<(), Error> { self.term.clear_screen()?; Ok(()) diff --git a/src/util.rs b/src/util.rs index f864bcfc7..2c22a4c11 100644 --- a/src/util.rs +++ b/src/util.rs @@ -32,7 +32,7 @@ pub enum KeywordClass { pub fn find_partial>( columns: &[&dyn Column], - pid: i32, + pid: i64, keyword: &[T], logic: &ConfigSearchLogic, case: &ConfigSearchCase, @@ -78,7 +78,7 @@ pub fn find_partial>( pub fn find_exact>( columns: &[&dyn Column], - pid: i32, + pid: i64, keyword: &[T], logic: &ConfigSearchLogic, case: &ConfigSearchCase, @@ -288,14 +288,16 @@ pub unsafe fn get_sys_value( ) -> bool { mib[0] = high as i32; mib[1] = low as i32; - libc::sysctl( - mib.as_mut_ptr(), - 2, - value, - &mut len as *mut usize, - ::std::ptr::null_mut(), - 0, - ) == 0 + unsafe { + libc::sysctl( + mib.as_mut_ptr(), + 2, + value, + &mut len as *mut usize, + ::std::ptr::null_mut(), + 0, + ) == 0 + } } pub fn bytify(x: u64) -> String { @@ -359,9 +361,7 @@ thread_local! { pub static USERS_CACHE: std::cell::RefCell = UsersCache::new().into(); } -#[cfg(target_os = "freebsd")] -// std::ffi::FromBytesUntilNulError is missing until Rust 1.73.0 -// https://github.com/rust-lang/rust/pull/113701 +#[cfg(any(target_os = "freebsd", target_os = "macos"))] pub fn ptr_to_cstr( x: &[std::os::raw::c_char], ) -> Result<&std::ffi::CStr, core::ffi::FromBytesUntilNulError> { @@ -371,16 +371,30 @@ pub fn ptr_to_cstr( std::ffi::CStr::from_bytes_until_nul(x) } +/// The four words of a FreeBSD `sigset_t`, which the signal columns print as +/// a mask. +/// +/// `libc` keeps the words private, so the first one cannot be named - but +/// `sigset_t` is `#[repr(C)]` around that one `[u32; 4]` field and nothing +/// else, so the words *are* the structure and can be read straight out of it. +#[cfg(target_os = "freebsd")] +pub fn sigset_words(set: &libc::sigset_t) -> [u32; 4] { + // SAFETY: `libc::sigset_t` is `#[repr(C)]` with a single `[u32; 4]` + // field, so it has that array's size (16) and alignment (4); this reads + // the structure's own bytes. + unsafe { *std::ptr::from_ref(set).cast::<[u32; 4]>() } +} + #[cfg(any(target_os = "linux", target_os = "android"))] pub fn process_new( - pid: i32, + pid: i64, procfs: &Option, ) -> procfs::ProcResult { if let Some(x) = procfs { let path = x.join(pid.to_string()); procfs::process::Process::new_with_root(path) } else { - procfs::process::Process::new(pid) + procfs::process::Process::new(pid as i32) } } @@ -429,4 +443,30 @@ mod tests { // Normal command lines are untouched assert_eq!(sanitize_control_chars("/bin/sleep 60"), "/bin/sleep 60"); } + + /// `sigset_words` has to read a `sigset_t` exactly the way libc's own + /// accessors do - that is the whole claim behind the pointer cast. Signals + /// are laid out lowest-numbered first, so 1..=32 land in the first word + /// (`_SIG_WORD(n) == (n - 1) / 32`). + /// + /// FreeBSD-only, and the CI runs the suite on a FreeBSD VM. + #[cfg(target_os = "freebsd")] + #[test] + fn sigset_words_agrees_with_sigismember() { + let mut set: libc::sigset_t = unsafe { std::mem::zeroed() }; + assert_eq!(unsafe { libc::sigemptyset(&mut set) }, 0); + assert_eq!(sigset_words(&set), [0; 4], "an empty set has no bits"); + + for signo in 1..=32 { + assert_eq!(unsafe { libc::sigaddset(&mut set, signo) }, 0); + } + + let words = sigset_words(&set); + assert_eq!(words[0], u32::MAX, "signals 1..=32 fill the first word"); + assert_eq!(words[1..], [0, 0, 0], "and nothing spills past it"); + for signo in 1..=32 { + assert_eq!(unsafe { libc::sigismember(&set, signo) }, 1); + } + assert_eq!(unsafe { libc::sigismember(&set, 33) }, 0); + } } diff --git a/src/view.rs b/src/view.rs index 0dc94aeca..65c9fb4a6 100644 --- a/src/view.rs +++ b/src/view.rs @@ -3,7 +3,7 @@ use crate::column::Column; use crate::columns::*; use crate::config::*; use crate::opt::{ArgColorMode, ArgPagerMode}; -use crate::process::collect_proc; +use crate::process::{ShowFilter, collect_proc, user_id_of}; use crate::search_regex::SearchRegex; use crate::style::{apply_color, apply_style, color_to_column_style}; use crate::term_info::TermInfo; @@ -26,10 +26,10 @@ pub struct View { pub columns: Vec, pub term_info: TermInfo, pub sort_info: SortInfo, - pub visible_pids: Vec, - pub auxiliary_pids: Vec, - pub parent_pids: HashMap, - pub child_pids: HashMap>, + pub visible_pids: Vec, + pub auxiliary_pids: Vec, + pub parent_pids: HashMap, + pub child_pids: HashMap>, } impl View { @@ -176,22 +176,38 @@ impl View { config.display.show_thread }; + // The command line wins over the configuration file, the way it does + // for the other options that have both. + let user_only = match &opt.show_user_only { + Some(user) => ConfigUserFilter::user(user), + None => config.display.show_user_only.clone(), + }; + let filter = ShowFilter { + userid: user_id_of(&user_only)?, + kthread: config.display.show_kthreads, + }; + let proc = collect_proc( Duration::from_millis(opt.interval), show_thread, - config.display.show_kthreads, &opt.procfs, + filter, ); - for c in columns.iter_mut() { - for p in &proc { - c.column.add(p); - } - } let mut parent_pids = HashMap::new(); - let mut child_pids = HashMap::>::new(); - if opt.tree || !config.display.show_self_parents { - for p in &proc { + let mut child_pids = HashMap::>::new(); + + // Every process is consumed before the next one is collected, so the + // per-process resources a platform holds - one `/proc/` directory + // handle per process on Linux - are released as we go instead of + // piling up until the whole listing has been read. With a low + // `RLIMIT_NOFILE` the old order ran out of descriptors partway and + // dropped the rest of the processes without a word. + for p in proc { + for c in columns.iter_mut() { + c.column.add(&p); + } + if opt.tree || !config.display.show_self_parents { parent_pids.insert(p.pid, p.ppid); if let Some(x) = child_pids.get_mut(&p.ppid) { x.push(p.pid); @@ -271,7 +287,7 @@ impl View { .column .sorted_pid(&self.sort_info.order); - let self_pid = std::process::id() as i32; + let self_pid = std::process::id() as i64; let self_parents = if !config.display.show_self_parents { let mut self_parents = Vec::new(); @@ -369,7 +385,7 @@ impl View { Ok(()) } - fn get_parent_pids(&self, pid: i32, parent_pids: &mut Vec) { + fn get_parent_pids(&self, pid: i64, parent_pids: &mut Vec) { if let Some(x) = self.parent_pids.get(&pid) && !parent_pids.contains(x) { @@ -378,7 +394,7 @@ impl View { } } - fn get_child_pids(&self, pid: i32, child_pids: &mut Vec) { + fn get_child_pids(&self, pid: i64, child_pids: &mut Vec) { if let Some(pids) = self.child_pids.get(&pid) { for x in pids { if !child_pids.contains(x) { @@ -434,11 +450,14 @@ impl View { usize::MIN }; - let use_builtin_pager = if cfg!(target_os = "windows") { - true - } else { - config.pager.use_builtin - }; + // On Windows, `[pager] command` is what selects the external pager, so + // it wins over the built-in one that is otherwise always available - + // unless `use_builtin` is set, which is an explicit request for the + // built-in pager. + #[cfg(target_os = "windows")] + let use_builtin_pager = config.pager.use_builtin || pager_command(config).is_none(); + #[cfg(not(target_os = "windows"))] + let use_builtin_pager = config.pager.use_builtin; let use_pager = match (opt.watch_mode, opt.pager.as_ref(), &config.pager.mode) { (true, _, _) => false, @@ -456,8 +475,7 @@ impl View { (false, None, ConfigPagerMode::Disable) => false, }; - // Minus doesn't support horizontal scroll yet - // https://github.com/arijit79/minus/issues/59 + // Minus support of horizontal scroll seems broken with ANSI escape code let cut_to_pager = if use_builtin_pager { true } else { @@ -493,7 +511,7 @@ impl View { if use_builtin_pager { self.term_info.use_pager = true; } else { - View::pager(config); + self.pager(config)?; } } @@ -516,6 +534,8 @@ impl View { if self.term_info.use_pager { minus::page_all(self.term_info.pager.replace(None).unwrap())?; + } else { + self.term_info.finish_external_pager()?; } Ok(()) @@ -573,7 +593,7 @@ impl View { fn display_content( &self, config: &Config, - pid: i32, + pid: i64, theme: &ConfigTheme, auxiliary: bool, ) -> Result<(), Error> { @@ -657,7 +677,7 @@ impl View { } fn search>( - pid: i32, + pid: i64, keyword_numeric: &[T], keyword_nonnumeric: &[T], cols_numeric: &[&dyn Column], @@ -705,7 +725,7 @@ impl View { } } - fn search_regex(pid: i32, cols: &[&dyn Column], regex: &SearchRegex) -> Result { + fn search_regex(pid: i64, cols: &[&dyn Column], regex: &SearchRegex) -> Result { for c in cols { if regex.is_match(&c.display_json(pid))? { return Ok(true); @@ -715,7 +735,7 @@ impl View { } #[cfg(not(any(target_os = "windows", any(target_os = "linux", target_os = "android"))))] - fn pager(config: &Config) { + fn pager(&mut self, config: &Config) -> Result<(), Error> { if let Some(ref pager) = config.pager.command { Pager::with_pager(pager).setup(); } else if which::which("less").is_ok() { @@ -723,10 +743,11 @@ impl View { } else { Pager::with_pager("more -f").setup(); } + Ok(()) } #[cfg(any(target_os = "linux", target_os = "android"))] - fn pager(config: &Config) { + fn pager(&mut self, config: &Config) -> Result<(), Error> { if let Some(ref pager) = config.pager.command { Pager::with_pager(pager) // workaround for default less charset is "ascii" on some environments (ex. Ubuntu) @@ -739,10 +760,45 @@ impl View { } else { Pager::with_pager("more -f").setup(); } + Ok(()) } + /// Spawns the pager given by `[pager] command` and feeds the output to its stdin. + /// If it is not set, or the command cannot be parsed or spawned, + /// the built-in pager is used. #[cfg(target_os = "windows")] - fn pager(_config: &Config) {} + fn pager(&mut self, config: &Config) -> Result<(), Error> { + let Some(command) = pager_command(config) else { + // `[pager] command` is not set: the built-in pager is the normal choice + self.term_info.use_pager = true; + return Ok(()); + }; + + let Some((program, args)) = split_pager_command(command) else { + let _ = console::Term::stderr().write_line(&format!( + "warning: failed to parse pager command \"{command}\". falling back to the built-in pager" + )); + self.term_info.use_pager = true; + return Ok(()); + }; + + match std::process::Command::new(&program) + .args(&args) + .stdin(std::process::Stdio::piped()) + .spawn() + { + Ok(child) => { + self.term_info.external_pager.replace(Some(child)); + } + Err(x) => { + let _ = console::Term::stderr().write_line(&format!( + "warning: failed to launch pager \"{program}\" ({x}). falling back to the built-in pager" + )); + self.term_info.use_pager = true; + } + } + Ok(()) + } pub fn inc_sort_column(&mut self) -> usize { let current = self.sort_info.idx; @@ -782,9 +838,79 @@ fn json_object(fields: &[String]) -> String { format!("{{{}}}", fields.join(", ")) } +/// Returns the pager command given by `command` of `[pager]` section. +/// An empty value is treated as unset. +#[cfg(target_os = "windows")] +fn pager_command(config: &Config) -> Option<&str> { + config + .pager + .command + .as_deref() + .map(str::trim) + .filter(|x| !x.is_empty()) +} + +/// Splits a command line into the program and its arguments. +/// A part surrounded by `"` or `'` is kept as a single argument +/// so that a path containing spaces (ex. `"C:\Program Files\Git\usr\bin\less.exe" -SR`) works. +/// Returns `None` if the command is empty or a quote is not closed, +/// so that the caller can fall back to the built-in pager. +#[cfg(target_os = "windows")] +fn split_pager_command(command: &str) -> Option<(String, Vec)> { + let mut args = Vec::new(); + let mut arg = String::new(); + let mut started = false; + let mut quote = None; + + for c in command.chars() { + match quote { + Some(q) => { + if c == q { + quote = None; + } else { + arg.push(c); + } + } + None => match c { + q @ ('"' | '\'') => { + started = true; + quote = Some(q); + } + c if c.is_whitespace() => { + if started { + args.push(std::mem::take(&mut arg)); + started = false; + } + } + _ => { + arg.push(c); + started = true; + } + }, + } + } + if quote.is_some() { + return None; + } + if started { + args.push(arg); + } + + let mut args = args.into_iter(); + let program = args.next()?; + if program.is_empty() { + return None; + } + Some((program, args.collect())) +} + #[cfg(test)] mod tests { use super::json_object; + #[cfg(target_os = "windows")] + use super::{pager_command, split_pager_command}; + #[cfg(target_os = "windows")] + use crate::{CONFIG_DEFAULT, Config}; #[test] fn json_object_skips_columns_without_json() { @@ -800,4 +926,66 @@ mod tests { r#"{"PID": 1, "CPU": 0}"# ); } + + #[cfg(target_os = "windows")] + #[test] + fn split_pager_command_parses_program_and_args() { + assert_eq!(split_pager_command(""), None); + assert_eq!(split_pager_command(" "), None); + assert_eq!(split_pager_command("less"), Some(("less".into(), vec![]))); + assert_eq!( + split_pager_command("less -SR"), + Some(("less".into(), vec!["-SR".into()])) + ); + assert_eq!( + split_pager_command(" less -S -R "), + Some(("less".into(), vec!["-S".into(), "-R".into()])) + ); + assert_eq!( + split_pager_command(r#""C:\Program Files\Git\usr\bin\less.exe" -SR"#), + Some(( + r"C:\Program Files\Git\usr\bin\less.exe".into(), + vec!["-SR".into()] + )) + ); + // single quotes work as well + assert_eq!( + split_pager_command(r"'C:\Program Files\Git\usr\bin\less.exe' -S -R"), + Some(( + r"C:\Program Files\Git\usr\bin\less.exe".into(), + vec!["-S".into(), "-R".into()] + )) + ); + // a quoted argument keeps the whitespaces in it + assert_eq!( + split_pager_command(r#"less "-S -R""#), + Some(("less".into(), vec!["-S -R".into()])) + ); + // unbalanced quote is rejected instead of building a broken command line + assert_eq!( + split_pager_command(r#""C:\Program Files\Git\usr\bin\less.exe -SR"#), + None + ); + assert_eq!(split_pager_command(r"less '-SR"), None); + } + + #[cfg(target_os = "windows")] + #[test] + fn pager_command_uses_config_command() { + let mut config: Config = toml::from_str(CONFIG_DEFAULT).unwrap(); + assert_eq!(pager_command(&config), None); + + config.pager.command = Some("less -SR".into()); + assert_eq!(pager_command(&config), Some("less -SR")); + + config.pager.command = Some(" less -SR ".into()); + assert_eq!(pager_command(&config), Some("less -SR")); + + // an empty `command` is treated as unset + config.pager.command = Some(" ".into()); + assert_eq!(pager_command(&config), None); + + config.pager.command = Some(String::new()); + assert_eq!(pager_command(&config), None); + } } diff --git a/tests/low_fd_limit.rs b/tests/low_fd_limit.rs new file mode 100644 index 000000000..c286e934c --- /dev/null +++ b/tests/low_fd_limit.rs @@ -0,0 +1,183 @@ +#![cfg(any(target_os = "linux", target_os = "android"))] + +use std::collections::BTreeSet; +use std::fs; +use std::io; +use std::os::unix::process::CommandExt; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; + +struct Fixture(PathBuf); + +impl Fixture { + fn new() -> Self { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let path = + std::env::temp_dir().join(format!("procs-low-fd-{}-{unique}", std::process::id())); + fs::create_dir(&path).unwrap(); + Self(path) + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn write_stat(path: &Path, pid: i32, ppid: i32, kernel_thread: bool) { + let mut fields = vec!["0".to_string(); 49]; + fields[0] = ppid.to_string(); // ppid, field 4 + if kernel_thread { + fields[5] = "2097152".to_string(); // PF_KTHREAD, field 9 + } + fields[16] = "2".to_string(); // num_threads, field 20 + fields[18] = "1".to_string(); // starttime, field 22 + fs::write(path, format!("{pid} (comm-{pid}) S {}\n", fields.join(" "))).unwrap(); +} + +fn run_procs(root: &Path, args: &[&str], fd_limit: libc::rlim_t) -> String { + let mut command = Command::new(env!("CARGO_BIN_EXE_procs")); + command + .arg("--load-config") + .arg(root.join("config.toml")) + .arg("--procfs") + .arg(root.join("proc")) + .args([ + "--color", + "disable", + "--theme", + "dark", + "--pager", + "disable", + "--no-header", + "--interval", + "0", + ]) + .args(args); + + // Only the child running procs gets a lower limit. The pre-exec callback + // uses resource-limit syscalls without allocation or locks after fork. + unsafe { + command.pre_exec(move || { + let mut limit = libc::rlimit { + rlim_cur: 0, + rlim_max: 0, + }; + if libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) != 0 { + return Err(io::Error::last_os_error()); + } + limit.rlim_cur = fd_limit.min(limit.rlim_max); + if libc::setrlimit(libc::RLIMIT_NOFILE, &limit) != 0 { + return Err(io::Error::last_os_error()); + } + Ok(()) + }); + } + + let output = command.output().unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert!( + output.stderr.is_empty(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + String::from_utf8(output.stdout).unwrap() +} + +#[test] +fn lists_all_processes_with_a_low_file_descriptor_limit() { + let fixture = Fixture::new(); + let procfs = fixture.0.join("proc"); + fs::create_dir(&procfs).unwrap(); + let pids = 900001..900097; + for pid in pids.clone() { + let process = procfs.join(pid.to_string()); + fs::create_dir(&process).unwrap(); + let ppid = if pid == pids.start { 1 } else { pids.start }; + write_stat(&process.join("stat"), pid, ppid, pid >= 900089); + fs::write( + process.join("cmdline"), + format!("fixture-{pid}\0--from-cmdline\0"), + ) + .unwrap(); + for tid in [pid, pid + 1000000] { + let task = process.join("task").join(tid.to_string()); + fs::create_dir_all(&task).unwrap(); + write_stat(&task.join("stat"), tid, ppid, false); + } + } + + for (args, with_threads, show_kthreads) in [ + (vec![], false, true), + (vec!["--thread"], true, true), + (vec!["--tree"], true, true), + (vec!["--thread"], true, false), + ] { + fs::write( + fixture.0.join("config.toml"), + format!( + r#"[[columns]] +kind = "Pid" +[[columns]] +kind = "TreeSlot" +[[columns]] +kind = "Command" +[display] +show_self = true +show_self_parents = true +show_kthreads = {show_kthreads} +"#, + ), + ) + .unwrap(); + let expected: BTreeSet<_> = pids + .clone() + .filter(|&pid| show_kthreads || pid < 900089) + .flat_map(|pid| { + let mut ids = vec![pid]; + if with_threads { + ids.push(pid + 1000000); + } + ids + }) + .collect(); + let control = run_procs(&fixture.0, &args, 1024); + let output = run_procs(&fixture.0, &args, 32); + let actual: BTreeSet = output + .lines() + .map(|line| { + line.split_whitespace() + .find_map(|word| word.trim_matches(['[', ']']).parse().ok()) + .unwrap_or_else(|| panic!("no PID in {line:?}, args={args:?}")) + }) + .collect(); + assert_eq!( + actual, expected, + "args={args:?}, show_kthreads={show_kthreads}" + ); + for pid in &expected { + let command = if pids.contains(pid) { + format!("fixture-{pid} --from-cmdline") + } else { + format!("comm-{pid}") + }; + assert!( + output.contains(&command), + "missing command for {pid}, args={args:?}" + ); + } + assert_eq!( + output, control, + "args={args:?}, show_kthreads={show_kthreads}" + ); + } +}