diff --git a/.gitignore b/.gitignore index ea8c4bf..5b3bd55 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -/target +syntax: glob + +/target/ diff --git a/Cargo.lock b/Cargo.lock index 8a41289..8310026 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,9 +84,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" [[package]] name = "block-buffer" @@ -103,17 +103,11 @@ version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "cc" -version = "1.2.13" +version = "1.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7777341816418c02e033934a09f20dc0ccaf65a5201ef8a450ae0105a573fda" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" dependencies = [ "shlex", ] @@ -261,9 +255,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] @@ -298,15 +292,15 @@ dependencies = [ [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" @@ -315,7 +309,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -336,14 +330,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" dependencies = [ "cfg-if", "libc", + "r-efi", "wasi", - "windows-targets 0.52.6", ] [[package]] @@ -389,9 +383,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" dependencies = [ "equivalent", "hashbrown", @@ -405,9 +399,9 @@ checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" @@ -439,9 +433,9 @@ checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" [[package]] name = "log" -version = "0.4.25" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" [[package]] name = "md-5" @@ -521,9 +515,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "once_cell" -version = "1.20.3" +version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" +checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" [[package]] name = "os_display" @@ -600,11 +594,11 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.7.35", + "zerocopy", ] [[package]] @@ -619,9 +613,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] @@ -650,13 +644,19 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.8.5" @@ -673,8 +673,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha", - "rand_core 0.9.0", - "zerocopy 0.8.17", + "rand_core 0.9.3", + "zerocopy", ] [[package]] @@ -684,7 +684,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -695,14 +695,19 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "rand_core" -version = "0.9.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" dependencies = [ "getrandom", - "zerocopy 0.8.17", ] +[[package]] +name = "rangemap" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60fcc7d6849342eff22c4350c8b9a989ee8ceabc4b481253e8946b9fe83d684" + [[package]] name = "rayon" version = "1.10.0" @@ -777,33 +782,33 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustix" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f8dcd64f141950290e45c99f7710ede1b600297c91818bb30b3667c0f45dc0" +checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys 0.9.3", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" [[package]] name = "ryu" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "serde" @@ -874,15 +879,21 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.98" +version = "2.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "syscall-numbers" +version = "4.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e88dcf8be2fd556b3cebd02689c424dced834317c7e38328eadfcfcab00b785" + [[package]] name = "sysinfo" version = "0.33.1" @@ -906,17 +917,17 @@ dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix 1.0.0", - "windows-sys 0.52.0", + "rustix 1.0.3", + "windows-sys 0.59.0", ] [[package]] name = "terminal_size" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5352447f921fda68cf61b4101566c0bdb5104eff6804d0678e5227580ab6a4e9" +checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "rustix 0.38.44", + "rustix 1.0.3", "windows-sys 0.59.0", ] @@ -933,10 +944,30 @@ dependencies = [ ] [[package]] -name = "time" -version = "0.3.37" +name = "thiserror" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618" dependencies = [ "deranged", "itoa", @@ -951,15 +982,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04" dependencies = [ "num-conv", "time-core", @@ -973,9 +1004,9 @@ checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "unicode-ident" -version = "1.0.16" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a210d160f08b701c8721ba1c726c11662f877ea6b7094007e1ca9a1041945034" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-linebreak" @@ -1023,6 +1054,7 @@ dependencies = [ "tempfile", "textwrap", "uu_blockdev", + "uu_chcpu", "uu_ctrlaltdel", "uu_dmesg", "uu_fsfreeze", @@ -1051,6 +1083,18 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_chcpu" +version = "0.0.1" +dependencies = [ + "clap", + "libc", + "rangemap", + "syscall-numbers", + "thiserror", + "uucore", +] + [[package]] name = "uu_ctrlaltdel" version = "0.0.1" @@ -1223,9 +1267,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" -version = "0.13.3+wasi-0.2.2" +version = "0.14.2+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" dependencies = [ "wit-bindgen-rt", ] @@ -1374,9 +1418,9 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-result" @@ -1537,9 +1581,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "wit-bindgen-rt" -version = "0.33.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] @@ -1551,7 +1595,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e" dependencies = [ "libc", - "rustix 1.0.0", + "rustix 1.0.3", ] [[package]] @@ -1562,39 +1606,18 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713" -dependencies = [ - "zerocopy-derive 0.8.17", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" +checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 5bad5d9..cd53738 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ uudoc = [] feat_common_core = [ "blockdev", + "chcpu", "ctrlaltdel", "dmesg", "fsfreeze", @@ -60,8 +61,11 @@ serde_json = { version = "1.0.122", features = ["preserve_order"] } sysinfo = "0.33" tempfile = "3.9.0" textwrap = { version = "0.16.0", features = ["terminal_size"] } +thiserror = "2.0" uucore = "0.0.30" xattr = "1.3.1" +rangemap = "1.5.1" +syscall-numbers = "4.0.2" [dependencies] clap = { workspace = true } @@ -76,6 +80,7 @@ uucore = { workspace = true } # blockdev = { optional = true, version = "0.0.1", package = "uu_blockdev", path = "src/uu/blockdev" } +chcpu = { optional = true, version = "0.0.1", package = "uu_chcpu", path = "src/uu/chcpu" } ctrlaltdel = { optional = true, version = "0.0.1", package = "uu_ctrlaltdel", path = "src/uu/ctrlaltdel" } dmesg = { optional = true, version = "0.0.1", package = "uu_dmesg", path = "src/uu/dmesg" } fsfreeze = { optional = true, version = "0.0.1", package = "uu_fsfreeze", path = "src/uu/fsfreeze" } diff --git a/src/uu/chcpu/Cargo.toml b/src/uu/chcpu/Cargo.toml new file mode 100644 index 0000000..9599c06 --- /dev/null +++ b/src/uu/chcpu/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "uu_chcpu" +version = "0.0.1" +edition = "2024" + +[lib] +path = "src/chcpu.rs" + +[[bin]] +name = "chcpu" +path = "src/main.rs" + +[dependencies] +clap = { workspace = true } +libc = { workspace = true } +rangemap = { workspace = true } +syscall-numbers = { workspace = true } +thiserror = { workspace = true } +uucore = { workspace = true } diff --git a/src/uu/chcpu/chcpu.md b/src/uu/chcpu/chcpu.md new file mode 100644 index 0000000..14d273a --- /dev/null +++ b/src/uu/chcpu/chcpu.md @@ -0,0 +1,11 @@ +# chcpu + +``` +chcpu {-e|--enable|-d|--disable|-c|--configure|-g|--deconfigure} cpu-list +chcpu {-p|--dispatch} mode +chcpu {-r|--rescan} +chcpu {-V|--version} +chcpu {-h|--help} +``` + +configure CPUs in a multi-processor system. diff --git a/src/uu/chcpu/src/chcpu.rs b/src/uu/chcpu/src/chcpu.rs new file mode 100644 index 0000000..810f5a8 --- /dev/null +++ b/src/uu/chcpu/src/chcpu.rs @@ -0,0 +1,338 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// Remove this if the tool is ported to Non-UNIX platforms. +#![cfg_attr(not(unix), allow(dead_code))] + +mod errors; +#[cfg(unix)] +mod sysfs; + +use std::str::FromStr; +use std::{fmt, str}; + +use clap::builder::{EnumValueParser, PossibleValue}; +use clap::{Arg, ArgAction, ArgGroup, Command, ValueEnum, crate_version}; +use rangemap::RangeInclusiveSet; +use uucore::{error::UResult, format_usage, help_about, help_usage}; + +use crate::errors::ChCpuError; + +mod options { + pub static ENABLE: &str = "enable"; + pub static DISABLE: &str = "disable"; + pub static CONFIGURE: &str = "configure"; + pub static DECONFIGURE: &str = "deconfigure"; + pub static CPU_LIST: &str = "cpu-list"; + pub static DISPATCH: &str = "dispatch"; + pub static MODE: &str = "mode"; + pub static RESCAN: &str = "rescan"; +} + +const ABOUT: &str = help_about!("chcpu.md"); +const USAGE: &str = help_usage!("chcpu.md"); + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let args = uu_app().try_get_matches_from_mut(args)?; + + if args.contains_id(options::ENABLE) { + let cpu_list = args + .get_one::<CpuList>(options::ENABLE) + .expect("cpu-list is required"); + + enable_cpu(cpu_list, true)?; + } else if args.contains_id(options::DISABLE) { + let cpu_list = args + .get_one::<CpuList>(options::DISABLE) + .expect("cpu-list is required"); + + enable_cpu(cpu_list, false)?; + } else if args.contains_id(options::CONFIGURE) { + let cpu_list = args + .get_one::<CpuList>(options::CONFIGURE) + .expect("cpu-list is required"); + + configure_cpu(cpu_list, true)?; + } else if args.contains_id(options::DECONFIGURE) { + let cpu_list = args + .get_one::<CpuList>(options::DECONFIGURE) + .expect("cpu-list is required"); + + configure_cpu(cpu_list, false)?; + } else if args.contains_id(options::DISPATCH) { + let dispatch_mode = args + .get_one::<DispatchMode>(options::DISPATCH) + .expect("mode is required"); + + set_dispatch_mode(*dispatch_mode)?; + } else if args.get_flag(options::RESCAN) { + rescan_cpus()?; + } else { + unimplemented!(); + } + + Ok(()) +} + +impl ValueEnum for DispatchMode { + fn value_variants<'a>() -> &'a [Self] { + &[Self::Horizontal, Self::Vertical] + } + + fn to_possible_value<'a>(&self) -> Option<PossibleValue> { + Some(match self { + Self::Horizontal => { + PossibleValue::new("horizontal").help("workload spread across all available CPUs") + } + Self::Vertical => { + PossibleValue::new("vertical").help("workload concentrated on few CPUs") + } + }) + } +} + +pub fn uu_app() -> Command { + Command::new(uucore::util_name()) + .version(crate_version!()) + .about(ABOUT) + .override_usage(format_usage(USAGE)) + .infer_long_args(true) + .arg_required_else_help(true) + .arg( + Arg::new(options::ENABLE) + .short('e') + .long(options::ENABLE) + .value_name(options::CPU_LIST) + .value_parser(CpuList::from_str) + .action(ArgAction::Set) + .help("enable CPUs"), + ) + .arg( + Arg::new(options::DISABLE) + .short('d') + .long(options::DISABLE) + .value_name(options::CPU_LIST) + .value_parser(CpuList::from_str) + .action(ArgAction::Set) + .help("disable CPUs"), + ) + .arg( + Arg::new(options::CONFIGURE) + .short('c') + .long(options::CONFIGURE) + .value_name(options::CPU_LIST) + .value_parser(CpuList::from_str) + .action(ArgAction::Set) + .help("configure CPUs"), + ) + .arg( + Arg::new(options::DECONFIGURE) + .short('g') + .long(options::DECONFIGURE) + .value_name(options::CPU_LIST) + .value_parser(CpuList::from_str) + .action(ArgAction::Set) + .help("deconfigure CPUs"), + ) + .arg( + Arg::new(options::DISPATCH) + .short('p') + .long(options::DISPATCH) + .value_name(options::MODE) + .value_parser(EnumValueParser::<DispatchMode>::new()) + .action(ArgAction::Set) + .help("set dispatching mode"), + ) + .arg( + Arg::new(options::RESCAN) + .short('r') + .long(options::RESCAN) + .action(ArgAction::SetTrue) + .help("trigger rescan of CPUs"), + ) + .group( + ArgGroup::new("control-group") + .args([ + options::ENABLE, + options::DISABLE, + options::CONFIGURE, + options::DECONFIGURE, + ]) + .multiple(false) + .conflicts_with_all(["dispatch-group", "rescan-group"]), + ) + .group( + ArgGroup::new("dispatch-group") + .args([options::DISPATCH]) + .multiple(false) + .conflicts_with_all(["control-group", "rescan-group"]), + ) + .group( + ArgGroup::new("rescan-group") + .args([options::RESCAN]) + .multiple(false) + .conflicts_with_all(["control-group", "dispatch-group"]), + ) + .after_help( + "<cpu-list> is one or more elements separated by commas. \ + Each element is either a positive integer (e.g., 3), \ + or an inclusive range of positive integers (e.g., 0-5). \ + For example, 0,2,7,10-13 refers to CPUs whose addresses are: 0, 2, 7, 10, 11, 12, and 13.", + ) +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[repr(u8)] +enum DispatchMode { + Horizontal = 0, + Vertical = 1, +} + +impl fmt::Display for DispatchMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Horizontal => write!(f, "horizontal"), + Self::Vertical => write!(f, "vertical"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct CpuList(RangeInclusiveSet<usize>); + +impl CpuList { + fn run(&self, f: &mut dyn FnMut(usize) -> Result<(), ChCpuError>) -> Result<(), ChCpuError> { + use std::ops::RangeInclusive; + + let iter = self.0.iter().flat_map(RangeInclusive::to_owned).map(f); + + let (success_occurred, first_error) = + iter.fold((false, None), |(success_occurred, first_error), result| { + if let Err(err) = result { + eprintln!("{err}"); + (success_occurred, first_error.or(Some(err))) + } else { + (true, first_error) + } + }); + + if let Some(err) = first_error { + if success_occurred { + uucore::error::set_exit_code(64); // Partial success. + Ok(()) + } else { + Err(err) + } + } else { + Ok(()) + } + } +} + +impl TryFrom<&[u8]> for CpuList { + type Error = ChCpuError; + + fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> { + let set: RangeInclusiveSet<usize> = bytes + .split(|&b| b == b',') + .map(|element| { + // Parsing: ...,element,... + let mut iter = element.splitn(2, |&b| b == b'-').map(<[u8]>::trim_ascii); + let first = iter.next(); + (first, iter.next()) + }) + .map(|(first, last)| { + let first = first.ok_or(ChCpuError::EmptyCpuList)?; + let first: usize = str::from_utf8(first) + .map_err(|_r| ChCpuError::CpuSpecNotPositiveInteger)? + .parse() + .map_err(|_r| ChCpuError::CpuSpecNotPositiveInteger)?; + + if let Some(last) = last { + // Parsing: ...,first-last,... + let last = str::from_utf8(last) + .map_err(|_r| ChCpuError::CpuSpecNotPositiveInteger)? + .parse() + .map_err(|_r| ChCpuError::CpuSpecNotPositiveInteger)?; + + if first <= last { + Ok(first..=last) + } else { + Err(ChCpuError::CpuSpecFirstAfterLast) + } + } else { + Ok(first..=first) // Parsing: ...,first,... + } + }) + .collect::<Result<_, _>>()?; + + if set.is_empty() { + Err(ChCpuError::EmptyCpuList) + } else { + Ok(Self(set)) + } + } +} + +impl FromStr for CpuList { + type Err = ChCpuError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + Self::try_from(s.as_bytes()) + } +} + +#[cfg(unix)] +fn enable_cpu(cpu_list: &CpuList, enable: bool) -> Result<(), ChCpuError> { + let sysfs_cpu = sysfs::SysFSCpu::open()?; + + let mut enabled_cpu_list = sysfs_cpu.enabled_cpu_list().ok(); + + cpu_list.run(&mut move |cpu_index| { + sysfs_cpu.enable_cpu(enabled_cpu_list.as_mut(), cpu_index, enable) + }) +} + +#[cfg(not(unix))] +fn enable_cpu(_cpu_list: &CpuList, _enable: bool) -> Result<(), ChCpuError> { + unimplemented!() +} + +#[cfg(unix)] +fn configure_cpu(cpu_list: &CpuList, configure: bool) -> Result<(), ChCpuError> { + let sysfs_cpu = sysfs::SysFSCpu::open()?; + + let enabled_cpu_list = sysfs_cpu.enabled_cpu_list().ok(); + + cpu_list.run(&mut move |cpu_index| { + sysfs_cpu.configure_cpu(enabled_cpu_list.as_ref(), cpu_index, configure) + }) +} + +#[cfg(not(unix))] +fn configure_cpu(_cpu_list: &CpuList, _configure: bool) -> Result<(), ChCpuError> { + unimplemented!() +} + +#[cfg(unix)] +fn set_dispatch_mode(dispatch_mode: DispatchMode) -> Result<(), ChCpuError> { + sysfs::SysFSCpu::open()?.set_dispatch_mode(dispatch_mode) +} + +#[cfg(not(unix))] +fn set_dispatch_mode(_dispatch_mode: DispatchMode) -> Result<(), ChCpuError> { + unimplemented!() +} + +#[cfg(unix)] +fn rescan_cpus() -> Result<(), ChCpuError> { + sysfs::SysFSCpu::open()?.rescan_cpus() +} + +#[cfg(not(unix))] +fn rescan_cpus() -> Result<(), ChCpuError> { + unimplemented!() +} diff --git a/src/uu/chcpu/src/errors.rs b/src/uu/chcpu/src/errors.rs new file mode 100644 index 0000000..d26e753 --- /dev/null +++ b/src/uu/chcpu/src/errors.rs @@ -0,0 +1,92 @@ +// This file is part of the uutils hostname package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::path::PathBuf; + +#[derive(Debug, thiserror::Error)] +pub enum ChCpuError { + #[error("CPU {0} is enabled")] + CpuIsEnabled(usize), + + #[error("CPU {0} is not configurable")] + CpuNotConfigurable(usize), + + #[error("CPU {0} is not hot pluggable")] + CpuNotHotPluggable(usize), + + #[error("this system does not support rescanning of CPUs")] + CpuRescanUnsupported, + + #[error("first element of CPU list range is greater than its last element")] + CpuSpecFirstAfterLast, + + #[error("CPU list element is not a positive number")] + CpuSpecNotPositiveInteger, + + #[error("CPU list is empty")] + EmptyCpuList, + + #[error("CPU {0} does not exist")] + InvalidCpuIndex(usize), + + #[error("{0}: {1}")] + IO0(String, std::io::Error), + + #[error("{0} '{path}': {2}", path = .1.display())] + IO1(String, PathBuf, std::io::Error), + + #[error("only one CPU is enabled")] + OneCpuIsEnabled, + + #[error("data is not an integer '{0}'")] + NotInteger(String), + + #[error("this system does not support setting the dispatching mode of CPUs")] + SetCpuDispatchUnsupported, +} + +impl ChCpuError { + pub(crate) fn io0(message: impl Into<String>, error: std::io::Error) -> Self { + Self::IO0(message.into(), error) + } + + pub(crate) fn io1( + message: impl Into<String>, + path: impl Into<PathBuf>, + error: std::io::Error, + ) -> Self { + Self::IO1(message.into(), path.into(), error) + } + + pub(crate) fn with_io_message(self, message: impl Into<String>) -> Self { + match self { + Self::IO0(_, err) => Self::IO0(message.into(), err), + + Self::IO1(_, path, err) => Self::IO1(message.into(), path, err), + + Self::CpuIsEnabled(_) + | Self::CpuNotConfigurable(_) + | Self::CpuNotHotPluggable(_) + | Self::CpuRescanUnsupported + | Self::CpuSpecFirstAfterLast + | Self::CpuSpecNotPositiveInteger + | Self::EmptyCpuList + | Self::InvalidCpuIndex(_) + | Self::OneCpuIsEnabled + | Self::NotInteger(_) + | Self::SetCpuDispatchUnsupported => self, + } + } +} + +impl uucore::error::UError for ChCpuError { + fn code(&self) -> i32 { + 1 + } + + fn usage(&self) -> bool { + false + } +} diff --git a/src/uu/chcpu/src/main.rs b/src/uu/chcpu/src/main.rs new file mode 100644 index 0000000..fe72ddc --- /dev/null +++ b/src/uu/chcpu/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_chcpu); diff --git a/src/uu/chcpu/src/sysfs.rs b/src/uu/chcpu/src/sysfs.rs new file mode 100644 index 0000000..e06a268 --- /dev/null +++ b/src/uu/chcpu/src/sysfs.rs @@ -0,0 +1,270 @@ +// This file is part of the uutils hostname package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::ffi::{CString, c_int}; +use std::fs::{File, OpenOptions}; +use std::io::{BufRead, BufReader, Read, Write, stdout}; +use std::os::fd::{AsRawFd, FromRawFd}; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::OpenOptionsExt; +use std::path::{Path, PathBuf}; +use std::str::FromStr; +use std::{fmt, str}; + +use crate::errors::ChCpuError; +use crate::{CpuList, DispatchMode}; + +pub(crate) const PATH_SYS_CPU: &str = "/sys/devices/system/cpu"; + +pub(crate) struct SysFSCpu(File); + +impl SysFSCpu { + pub(crate) fn open() -> Result<Self, ChCpuError> { + OpenOptions::new() + .read(true) + .custom_flags(libc::O_CLOEXEC) + .open(PATH_SYS_CPU) + .map(Self) + .map_err(|err| ChCpuError::io1("failed to open", PATH_SYS_CPU, err)) + } + + fn inner_path(name: impl AsRef<Path>) -> PathBuf { + Path::new(PATH_SYS_CPU).join(name) + } + + pub(crate) fn ensure_accessible( + &self, + name: impl AsRef<Path>, + access: c_int, + ) -> Result<(), ChCpuError> { + use std::io::Error; + + let name = name.as_ref(); + let c_name = c_string_from_path(name)?; + + if unsafe { libc::faccessat(self.0.as_raw_fd(), c_name.as_ptr(), access, 0) } == 0 { + Ok(()) + } else { + let path = Self::inner_path(name); + let err = Error::last_os_error(); + Err(ChCpuError::io1("file/directory is inaccessible", path, err)) + } + } + + pub(crate) fn open_inner( + &self, + name: impl AsRef<Path>, + flags: c_int, + ) -> Result<File, ChCpuError> { + use std::io::Error; + + let name = name.as_ref(); + let c_name = c_string_from_path(name)?; + + unsafe { + let fd = libc::openat(self.0.as_raw_fd(), c_name.as_ptr(), flags); + if fd >= 0 { + return Ok(File::from_raw_fd(fd)); + } + } + + let path = Self::inner_path(name); + let err = Error::last_os_error(); + Err(ChCpuError::io1("failed to open", path, err)) + } + + pub(crate) fn read_value<T>(&self, name: impl AsRef<Path>) -> Result<T, ChCpuError> + where + T: FromStr, + { + let name = name.as_ref(); + let mut line = String::default(); + + self.open_inner(name, libc::O_RDONLY | libc::O_CLOEXEC) + .map(BufReader::new)? + .read_line(&mut line) + .map_err(|err| ChCpuError::io1("failed to read file", Self::inner_path(name), err))?; + + line.trim() + .parse() + .map_err(|_r| ChCpuError::NotInteger(line.trim().into())) + } + + pub(crate) fn write_value( + &self, + name: impl AsRef<Path>, + value: impl fmt::Display, + ) -> Result<(), ChCpuError> { + let name = name.as_ref(); + + self.open_inner(name, libc::O_WRONLY | libc::O_CLOEXEC)? + .write_all(format!("{value}").as_bytes()) + .map_err(|err| ChCpuError::io1("failed to write file", Self::inner_path(name), err)) + } + + pub(crate) fn enabled_cpu_list(&self) -> Result<CpuList, ChCpuError> { + let mut buffer = Vec::default(); + + self.open_inner("online", libc::O_RDONLY | libc::O_CLOEXEC)? + .read_to_end(&mut buffer) + .map_err(|err| { + ChCpuError::io1("failed to read file", Self::inner_path("online"), err) + })?; + + CpuList::try_from(buffer.as_slice()) + } + + pub(crate) fn cpu_dir_path(&self, cpu_index: usize) -> Result<PathBuf, ChCpuError> { + let dir_name = PathBuf::from(format!("cpu{cpu_index}")); + + self.ensure_accessible(&dir_name, libc::F_OK) + .map(|()| dir_name) + .map_err(|_r| ChCpuError::InvalidCpuIndex(cpu_index)) + } + + pub(crate) fn enable_cpu( + &self, + enabled_cpu_list: Option<&mut CpuList>, + cpu_index: usize, + enable: bool, + ) -> Result<(), ChCpuError> { + use std::ops::RangeInclusive; + + let dir_name = self.cpu_dir_path(cpu_index)?; + + let online_path = dir_name.join("online"); + self.ensure_accessible(&online_path, libc::F_OK) + .map_err(|_r| ChCpuError::CpuNotHotPluggable(cpu_index))?; + + let online = self + .read_value::<i32>(&online_path) + .map(|value| value != 0)?; + + let new_state = if enable { "enabled" } else { "disabled" }; + + if enable == online { + let mut stdout = stdout().lock(); + return writeln!(&mut stdout, "CPU {cpu_index} is already {new_state}") + .map_err(|err| ChCpuError::io0("write standard output", err)); + } + + if let Some(enabled_cpu_list) = &enabled_cpu_list { + let iter = enabled_cpu_list + .0 + .iter() + .flat_map(RangeInclusive::to_owned) + .take(2); + + if !enable && iter.count() <= 1 { + return Err(ChCpuError::OneCpuIsEnabled); + } + } + + let configured = self.read_value::<i32>(dir_name.join("configure")); + + if let Err(err) = self.write_value(&online_path, u8::from(enable)) { + let operation = if enable { "enable" } else { "disable" }; + + let reason = if enable && configured.is_ok_and(|value| value == 0) { + " (CPU is deconfigured)" + } else { + "" + }; + + return Err(err.with_io_message(format!("CPU {cpu_index} {operation} failed{reason}"))); + } + + if let Some(enabled_cpu_list) = enabled_cpu_list { + if enable { + enabled_cpu_list.0.insert(cpu_index..=cpu_index); + } else { + enabled_cpu_list.0.remove(cpu_index..=cpu_index); + } + } + + let mut stdout = stdout().lock(); + writeln!(&mut stdout, "CPU {cpu_index} {new_state}",) + .map_err(|err| ChCpuError::io0("write standard output", err)) + } + + pub(crate) fn configure_cpu( + &self, + enabled_cpu_list: Option<&CpuList>, + cpu_index: usize, + configure: bool, + ) -> Result<(), ChCpuError> { + let dir_name = self.cpu_dir_path(cpu_index)?; + + let configure_path = dir_name.join("configure"); + self.ensure_accessible(&configure_path, libc::F_OK) + .map_err(|_r| ChCpuError::CpuNotConfigurable(cpu_index))?; + + let previous_config = self + .read_value::<i32>(&configure_path) + .map(|value| value != 0)?; + + let new_state = if configure { + "configured" + } else { + "deconfigured" + }; + + if configure == previous_config { + let mut stdout = stdout().lock(); + return writeln!(&mut stdout, "CPU {cpu_index} is already {new_state}") + .map_err(|err| ChCpuError::io0("write standard output", err)); + } + + if let Some(enabled_cpu_list) = enabled_cpu_list { + if previous_config && !configure && enabled_cpu_list.0.contains(&cpu_index) { + return Err(ChCpuError::CpuIsEnabled(cpu_index)); + } + } + + if let Err(err) = self.write_value(&configure_path, u8::from(configure)) { + let operation = if configure { + "configure" + } else { + "deconfigure" + }; + Err(err.with_io_message(format!("CPU {cpu_index} {operation} failed"))) + } else { + let mut stdout = stdout().lock(); + writeln!(&mut stdout, "CPU {cpu_index} {new_state}",) + .map_err(|err| ChCpuError::io0("write standard output", err)) + } + } + + pub(crate) fn set_dispatch_mode(&self, mode: DispatchMode) -> Result<(), ChCpuError> { + self.ensure_accessible("dispatching", libc::F_OK) + .map_err(|_r| ChCpuError::SetCpuDispatchUnsupported)?; + + self.write_value("dispatching", mode as u8) + .map_err(|err| err.with_io_message("failed to set dispatch mode"))?; + + let mut stdout = stdout().lock(); + writeln!(&mut stdout, "Successfully set {mode} dispatching mode") + .map_err(|err| ChCpuError::io0("write standard output", err)) + } + + pub(crate) fn rescan_cpus(&self) -> Result<(), ChCpuError> { + self.ensure_accessible("rescan", libc::F_OK) + .map_err(|_r| ChCpuError::CpuRescanUnsupported)?; + + self.write_value("rescan", "1") + .map_err(|err| err.with_io_message("failed to trigger rescan of CPUs"))?; + + let mut stdout = stdout().lock(); + writeln!(&mut stdout, "Triggered rescan of CPUs") + .map_err(|err| ChCpuError::io0("write standard output", err)) + } +} + +fn c_string_from_path(path: &Path) -> Result<CString, ChCpuError> { + use std::io::{Error, ErrorKind}; + + CString::new(path.as_os_str().as_bytes()) + .map_err(|_r| ChCpuError::io1("invalid name", path, Error::from(ErrorKind::InvalidInput))) +}