diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5e534f..a50d429 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,9 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - name: Install dependencies + shell: bash + run: case ${{ matrix.os }} in ubuntu-*) sudo apt-get update --quiet && sudo apt-get install --quiet --no-install-recommends --assume-yes libclang-dev libc6-dev libsmartcols-dev libmount-dev ;; esac; - run: cargo check test: @@ -26,6 +29,9 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - name: Install dependencies + shell: bash + run: case ${{ matrix.os }} in ubuntu-*) sudo apt-get update --quiet && sudo apt-get install --quiet --no-install-recommends --assume-yes libclang-dev libc6-dev libsmartcols-dev libmount-dev ;; esac; - run: cargo test fmt: @@ -34,6 +40,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - run: sudo apt-get update --quiet + - run: sudo apt-get install --quiet --no-install-recommends --assume-yes libclang-dev libc6-dev libsmartcols-dev libmount-dev - run: rustup component add rustfmt - run: cargo fmt --all -- --check @@ -46,6 +54,9 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - name: Install dependencies + shell: bash + run: case ${{ matrix.os }} in ubuntu-*) sudo apt-get update --quiet && sudo apt-get install --quiet --no-install-recommends --assume-yes libclang-dev libc6-dev libsmartcols-dev libmount-dev ;; esac; - run: rustup component add clippy - run: cargo clippy -- -D warnings @@ -79,6 +90,10 @@ jobs: CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' ) outputs CODECOV_FLAGS + - name: Install dependencies + shell: bash + run: case ${{ matrix.job.os }} in ubuntu-*) sudo apt-get update --quiet && sudo apt-get install --quiet --no-install-recommends --assume-yes libclang-dev libc6-dev libsmartcols-dev libmount-dev ;; esac; + - name: rust toolchain ~ install uses: dtolnay/rust-toolchain@nightly - name: Install llvm-tools-preview diff --git a/Cargo.lock b/Cargo.lock index 72a8529..3e5831e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,26 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "bindgen" +version = "0.71.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + [[package]] name = "bitflags" version = "2.9.0" @@ -105,13 +125,22 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "cc" -version = "1.2.16" +version = "1.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" dependencies = [ "shlex", ] +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -140,9 +169,9 @@ dependencies = [ [[package]] name = "chrono-tz" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6ac4f2c0bf0f44e9161aec9675e1050aa4a530663c4a9e37e108fa948bca9f" +checksum = "efdce149c370f133a071ca8ef6ea340b7b88748ab0810097a9e2976eaa34b4f3" dependencies = [ "chrono", "chrono-tz-build", @@ -151,14 +180,25 @@ dependencies = [ [[package]] name = "chrono-tz-build" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7" +checksum = "8f10f8c9340e31fc120ff885fcdb54a0b48e474bbd77cab557f0c30a3e569402" dependencies = [ "parse-zoneinfo", "phf_codegen", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + [[package]] name = "clap" version = "4.5.34" @@ -230,9 +270,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" dependencies = [ "powerfmt", ] @@ -265,6 +305,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" @@ -329,14 +381,15 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", "windows-core 0.52.0", ] @@ -366,6 +419,15 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.15" @@ -388,6 +450,16 @@ version = "0.2.171" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +[[package]] +name = "libloading" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -402,9 +474,9 @@ checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" [[package]] name = "log" -version = "0.4.26" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "md-5" @@ -422,6 +494,12 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "nix" version = "0.29.0" @@ -434,6 +512,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nom" version = "8.0.0" @@ -493,17 +581,17 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.1" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "os_display" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6229bad892b46b0dcfaaeb18ad0d2e56400f5aaea05b768bde96e73676cf75" +checksum = "ad5fd71b79026fb918650dde6d125000a233764f1c2f1659a1c71118e33ea08f" dependencies = [ - "unicode-width 0.1.14", + "unicode-width", ] [[package]] @@ -522,7 +610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4bffd1156cebf13f681d7769924d3edfb9d9d71ba206a8d8e8e7eb9df4f4b1e7" dependencies = [ "chrono", - "nom", + "nom 8.0.0", "regex", ] @@ -589,6 +677,16 @@ dependencies = [ "yansi", ] +[[package]] +name = "prettyplease" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro2" version = "1.0.94" @@ -730,6 +828,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3" +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + [[package]] name = "rustix" version = "0.38.44" @@ -768,6 +872,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" version = "1.0.219" @@ -813,6 +926,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "smartcols-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95f44699de6c620259d5a4fe55dc9e6a1a98af93f935567f82bf02fad5c2295d" +dependencies = [ + "bindgen", + "cc", + "dunce", + "walkdir", +] + [[package]] name = "smawk" version = "0.3.2" @@ -897,7 +1022,7 @@ dependencies = [ "smawk", "terminal_size", "unicode-linebreak", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] @@ -922,9 +1047,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.40" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -945,9 +1070,9 @@ checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -971,12 +1096,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "unicode-width" version = "0.2.0" @@ -1017,6 +1136,7 @@ dependencies = [ "uu_fsfreeze", "uu_last", "uu_lscpu", + "uu_lsipc", "uu_lslocks", "uu_lsmem", "uu_mcookie", @@ -1105,6 +1225,17 @@ dependencies = [ "uucore", ] +[[package]] +name = "uu_lsipc" +version = "0.0.1" +dependencies = [ + "clap", + "errno", + "libc", + "smartcols-sys", + "uucore", +] + [[package]] name = "uu_lslocks" version = "0.0.1" @@ -1222,6 +1353,16 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" @@ -1314,6 +1455,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -1563,18 +1713,18 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zerocopy" -version = "0.8.23" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd97444d05a4328b90e75e503a34bad781f14e28a823ad3557f0750df1ebcbc6" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.23" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6352c01d0edd5db859a63e2605f4ea3183ddbd15e2c4a9e7d32184df75e4f154" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 09330eb..3798c36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ feat_common_core = [ "fsfreeze", "last", "lscpu", + "lsipc", "lslocks", "lsmem", "mcookie", @@ -48,24 +49,26 @@ clap = { version = "4.4", features = ["wrap_help", "cargo"] } clap_complete = "4.4" clap_mangen = "0.2" dns-lookup = "2.0.4" -libc = "0.2.152" +errno = "0.3" +libc = "0.2.171" linux-raw-sys = { version = "0.9.0", features = ["ioctl"] } md-5 = "0.10.6" nix = { version = "0.29", default-features = false } phf = "0.11.2" phf_codegen = "0.11.2" rand = { version = "0.9.0", features = ["small_rng"] } +rangemap = "1.5.1" regex = "1.10.2" serde = { version = "1.0", features = ["derive"] } serde_json = { version = "1.0.122", features = ["preserve_order"] } +smartcols-sys = "0.1" +syscall-numbers = "4.0.2" sysinfo = "0.34" 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 } @@ -86,6 +89,7 @@ dmesg = { optional = true, version = "0.0.1", package = "uu_dmesg", path = "src/ fsfreeze = { optional = true, version = "0.0.1", package = "uu_fsfreeze", path = "src/uu/fsfreeze" } last = { optional = true, version = "0.0.1", package = "uu_last", path = "src/uu/last" } lscpu = { optional = true, version = "0.0.1", package = "uu_lscpu", path = "src/uu/lscpu" } +lsipc = { optional = true, version = "0.0.1", package = "uu_lsipc", path = "src/uu/lsipc" } lslocks = { optional = true, version = "0.0.1", package = "uu_lslocks", path = "src/uu/lslocks" } lsmem = { optional = true, version = "0.0.1", package = "uu_lsmem", path = "src/uu/lsmem" } mcookie = { optional = true, version = "0.0.1", package = "uu_mcookie", path = "src/uu/mcookie" } diff --git a/src/uu/lsipc/Cargo.toml b/src/uu/lsipc/Cargo.toml new file mode 100644 index 0000000..a849b4a --- /dev/null +++ b/src/uu/lsipc/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "uu_lsipc" +version = "0.0.1" +edition = "2024" + +[lib] +path = "src/lsipc.rs" + +[[bin]] +name = "lsipc" +path = "src/main.rs" + +[dependencies] +uucore = { workspace = true } +clap = { workspace = true } +libc = { workspace = true } +errno = { workspace = true } +smartcols-sys = { workspace = true } diff --git a/src/uu/lsipc/after-help.txt b/src/uu/lsipc/after-help.txt new file mode 100644 index 0000000..ac9be6a --- /dev/null +++ b/src/uu/lsipc/after-help.txt @@ -0,0 +1,43 @@ +Generic columns: + KEY Resource key + ID Resource ID + OWNER Owner's username or UID + PERMS Permissions + CUID Creator UID + CUSER Creator user + CGID Creator GID +CGROUP Creator group + UID User ID + USER User name + GID Group ID + GROUP Group name + CTIME Time of the last change + +Shared-memory columns (--shmems): + SIZE Segment size + NATTCH Number of attached processes + STATUS Status + ATTACH Attach time + DETACH Detach time +COMMAND Creator command line + CPID PID of the creator + LPID PID of last user + +Message-queue columns (--queues): +USEDBYTES Bytes used + MSGS Number of messages + SEND Time of last msg sent + RECV Time of last msg received + LSPID PID of the last msg sender + LRPID PID of the last msg receiver + +Semaphore columns (--semaphores): +NSEMS Number of semaphores +OTIME Time of the last operation + +Summary columns (--global): + RESOURCE Resource name +DESCRIPTION Resource description + LIMIT System-wide limit + USED Currently used + USE% Currently use percentage diff --git a/src/uu/lsipc/lsipc.md b/src/uu/lsipc/lsipc.md new file mode 100644 index 0000000..b9c5339 --- /dev/null +++ b/src/uu/lsipc/lsipc.md @@ -0,0 +1,10 @@ +# lsipc + +``` +lsipc [-g|--global] [{-m|--shmems|-q|--queues|-s|--semaphores} [-c|--creator] [-t|--time]] [-e|--export|-J|--json|-l|--list|-n|--newline|-r|--raw] [-b|--bytes] [--noheadings] [--notruncate] [-o list|--output list] [-P|--numeric-perms] [--time-format type] [-y|--shell] +lsipc {-i id|--id id} {-m|--shmems|-q|--queues|-s|--semaphores} [-e|--export|-J|--json|-l|--list|-n|--newline|-r|--raw] [-b|--bytes] [-c|--creator] [--noheadings] [--notruncate] [-o list|--output list] [-P|--numeric-perms] [-t|--time] [--time-format type] [-y|--shell] +lsipc {-V|--version} +lsipc {-h|--help} +``` + +show information on IPC facilities currently employed in the system. diff --git a/src/uu/lsipc/src/column.rs b/src/uu/lsipc/src/column.rs new file mode 100644 index 0000000..4ac180e --- /dev/null +++ b/src/uu/lsipc/src/column.rs @@ -0,0 +1,301 @@ +// 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. + +use std::ffi::{CStr, c_uint}; +use std::str::FromStr; + +use smartcols_sys::{SCOLS_FL_NOEXTREMES, SCOLS_FL_RIGHT, SCOLS_FL_TRUNC}; + +use crate::errors::LsIpcError; + +#[derive(Debug, Copy, Clone)] +pub(crate) struct ColumnInfo { + pub(crate) id: &'static CStr, + pub(crate) title: &'static str, + pub(crate) width_hint: f64, + pub(crate) flags: c_uint, +} + +impl ColumnInfo { + const fn new(id: &'static CStr, title: &'static str, flags: c_uint) -> Self { + Self { + id, + title, + width_hint: 1.0, + flags, + } + } +} + +pub(crate) static COLUMN_INFOS: [ColumnInfo; 34] = [ + // Generic + ColumnInfo::new(c"KEY", "Key", 0), + ColumnInfo::new(c"ID", "ID", 0), + ColumnInfo::new(c"OWNER", "Owner", SCOLS_FL_RIGHT), + ColumnInfo::new(c"PERMS", "Permissions", SCOLS_FL_RIGHT), + ColumnInfo::new(c"CUID", "Creator UID", SCOLS_FL_RIGHT), + ColumnInfo::new(c"CUSER", "Creator user", 0), + ColumnInfo::new(c"CGID", "Creator GID", SCOLS_FL_RIGHT), + ColumnInfo::new(c"CGROUP", "Creator group", 0), + ColumnInfo::new(c"UID", "UID", SCOLS_FL_RIGHT), + ColumnInfo::new(c"USER", "User name", 0), + ColumnInfo::new(c"GID", "GID", SCOLS_FL_RIGHT), + ColumnInfo::new(c"GROUP", "Group name", 0), + ColumnInfo::new(c"CTIME", "Last change", SCOLS_FL_RIGHT), + // Message queues + ColumnInfo::new(c"USEDBYTES", "Bytes used", SCOLS_FL_RIGHT), + ColumnInfo::new(c"MSGS", "Messages", 0), + ColumnInfo::new(c"SEND", "Msg sent", SCOLS_FL_RIGHT), + ColumnInfo::new(c"RECV", "Msg received", SCOLS_FL_RIGHT), + ColumnInfo::new(c"LSPID", "Msg sender", SCOLS_FL_RIGHT), + ColumnInfo::new(c"LRPID", "Msg receiver", SCOLS_FL_RIGHT), + // Shared memory + ColumnInfo::new(c"SIZE", "Segment size", SCOLS_FL_RIGHT), + ColumnInfo::new(c"NATTCH", "Attached processes", SCOLS_FL_RIGHT), + ColumnInfo::new(c"STATUS", "Status", SCOLS_FL_NOEXTREMES), + ColumnInfo::new(c"ATTACH", "Attach time", SCOLS_FL_RIGHT), + ColumnInfo::new(c"DETACH", "Detach time", SCOLS_FL_RIGHT), + ColumnInfo { + id: c"COMMAND", + title: "Creator command", + width_hint: 0.0, + flags: SCOLS_FL_TRUNC, + }, + ColumnInfo::new(c"CPID", "Creator PID", SCOLS_FL_RIGHT), + ColumnInfo::new(c"LPID", "Last user PID", SCOLS_FL_RIGHT), + // Semaphores + ColumnInfo::new(c"NSEMS", "Semaphores", SCOLS_FL_RIGHT), + ColumnInfo::new(c"OTIME", "Last operation", SCOLS_FL_RIGHT), + // Summary + ColumnInfo::new(c"RESOURCE", "Resource", 0), + ColumnInfo::new(c"DESCRIPTION", "Description", 0), + ColumnInfo::new(c"USED", "Used", SCOLS_FL_RIGHT), + ColumnInfo::new(c"USE%", "Use", SCOLS_FL_RIGHT), + ColumnInfo::new(c"LIMIT", "Limit", SCOLS_FL_RIGHT), +]; + +mod all { + pub(crate) static GENERIC: [&str; 13] = [ + "KEY", "ID", "OWNER", "PERMS", "CUID", "CUSER", "CGID", "CGROUP", "UID", "USER", "GID", + "GROUP", "CTIME", + ]; + + pub(crate) static QUEUES: [&str; 6] = ["USEDBYTES", "MSGS", "SEND", "RECV", "LSPID", "LRPID"]; + + pub(crate) static SHARED_MEMORY: [&str; 8] = [ + "SIZE", "NATTCH", "STATUS", "ATTACH", "DETACH", "COMMAND", "CPID", "LPID", + ]; + + pub(crate) static SEMAPHORES: [&str; 2] = ["NSEMS", "OTIME"]; + + pub(crate) static SUMMARY: [&str; 5] = ["RESOURCE", "DESCRIPTION", "USED", "USE%", "LIMIT"]; +} + +mod default { + pub(crate) static QUEUES: [&str; 8] = [ + "KEY", + "ID", + "PERMS", + "OWNER", + "USEDBYTES", + "MSGS", + "LSPID", + "LRPID", + ]; + + pub(crate) static SHARED_MEMORY: [&str; 11] = [ + "KEY", "ID", "PERMS", "OWNER", "SIZE", "NATTCH", "STATUS", "CTIME", "CPID", "LPID", + "COMMAND", + ]; + + pub(crate) static SEMAPHORES: [&str; 5] = ["KEY", "ID", "PERMS", "OWNER", "NSEMS"]; + + pub(crate) static GLOBAL: [&str; 5] = ["RESOURCE", "DESCRIPTION", "LIMIT", "USED", "USE%"]; + + pub(crate) static CREATOR: [&str; 4] = ["CUID", "CGID", "UID", "GID"]; +} + +#[derive(Debug, Clone)] +pub(crate) struct OutputColumns { + pub(crate) append: bool, + pub(crate) list: Vec<&'static ColumnInfo>, +} + +impl Default for OutputColumns { + fn default() -> Self { + Self { + append: true, + list: Vec::default(), + } + } +} + +impl FromStr for OutputColumns { + type Err = LsIpcError; + + fn from_str(s: &str) -> Result { + let suffix = s.strip_prefix('+'); + let append = suffix.is_some(); + + let list: Vec<_> = suffix + .unwrap_or(s) + .split(',') + .map(|name| { + COLUMN_INFOS + .iter() + .find(|&column| column.id.to_str().unwrap() == name) + .ok_or_else(|| LsIpcError::InvalidColumnName(name.into())) + }) + .collect::>()?; + + if list.is_empty() { + Err(LsIpcError::InvalidColumnSequence(s.into())) + } else { + Ok(Self { append, list }) + } + } +} + +impl From<&'_ clap::ArgMatches> for OutputColumns { + fn from(args: &'_ clap::ArgMatches) -> Self { + let Some(columns) = args.get_one::(crate::options::OUTPUT) else { + return Self::default(); + }; + + let allowed_column_names = if args.get_flag(crate::options::GLOBAL) { + &all::SUMMARY[..] + } else if args.get_flag(crate::options::QUEUES) { + &all::QUEUES[..] + } else if args.get_flag(crate::options::SHMEMS) { + &all::SHARED_MEMORY[..] + } else if args.get_flag(crate::options::SEMAPHORES) { + &all::SEMAPHORES[..] + } else { + unreachable!() + }; + + let (list, not_applicable): (Vec<&ColumnInfo>, Vec<&ColumnInfo>) = + columns.list.iter().partition(|&&column| { + let id = column.id.to_str().unwrap(); + all::GENERIC.contains(&id) || allowed_column_names.contains(&id) + }); + + if !not_applicable.is_empty() { + let join = move |mut buffer: String, column: &ColumnInfo| { + if !buffer.is_empty() { + buffer.push(','); + } + buffer.push_str(column.id.to_str().unwrap()); + buffer + }; + + let not_applicable = not_applicable.into_iter().fold(String::default(), join); + eprintln!("The following columns do not apply to the specified IPC: {not_applicable}."); + } + + Self { + append: columns.append, + list, + } + } +} + +pub(crate) fn all_defaults( + args: &clap::ArgMatches, +) -> Result, LsIpcError> { + let mut iter: Box> = Box::new(all::GENERIC.into_iter()); + + if args.get_flag(crate::options::QUEUES) { + iter = Box::new(iter.chain(all::QUEUES)); + } + + if args.get_flag(crate::options::SHMEMS) { + iter = Box::new(iter.chain(all::SHARED_MEMORY)); + } + + if args.get_flag(crate::options::SEMAPHORES) { + iter = Box::new(iter.chain(all::SEMAPHORES)); + } + + iter.map(|name| { + COLUMN_INFOS + .iter() + .find(|&column| column.id.to_str().unwrap() == name) + .ok_or_else(|| LsIpcError::InvalidColumnName(name.into())) + }) + .collect::>() +} + +pub(crate) fn filter_defaults( + args: &clap::ArgMatches, +) -> Result, LsIpcError> { + let mut columns = Vec::default(); + + if args.get_flag(crate::options::QUEUES) { + columns.extend(default::QUEUES); + } + + if args.get_flag(crate::options::SHMEMS) { + columns.extend(default::SHARED_MEMORY); + } + + if args.get_flag(crate::options::SEMAPHORES) { + columns.extend(default::SEMAPHORES); + } + + if args.get_flag(crate::options::GLOBAL) { + columns.extend(default::GLOBAL); + } + + if args.get_flag(crate::options::CREATOR) { + columns.extend(default::CREATOR); + } + + if args.get_flag(crate::options::TIME) { + if args.get_flag(crate::options::QUEUES) + || (!args.get_flag(crate::options::SHMEMS) + && !args.get_flag(crate::options::SEMAPHORES)) + { + columns.extend(["SEND", "RECV", "CTIME"]) + } + + if args.get_flag(crate::options::SHMEMS) + || (!args.get_flag(crate::options::QUEUES) + && !args.get_flag(crate::options::SEMAPHORES)) + { + // If "COMMAND" was the last column, then keep it last. + let reappend_command = match columns.pop() { + None => false, + Some("COMMAND") => true, + Some(last) => { + columns.push(last); + false + } + }; + + columns.extend(["ATTACH", "DETACH"]); + + if reappend_command { + columns.push("COMMAND"); + } + } + + if args.get_flag(crate::options::SEMAPHORES) + || (!args.get_flag(crate::options::QUEUES) && !args.get_flag(crate::options::SHMEMS)) + { + columns.extend(["OTIME", "CTIME"]) + } + } + + columns + .into_iter() + .map(|name| { + COLUMN_INFOS + .iter() + .find(|&column| column.id.to_str().unwrap() == name) + .ok_or_else(|| LsIpcError::InvalidColumnName(name.into())) + }) + .collect::>() +} diff --git a/src/uu/lsipc/src/display.rs b/src/uu/lsipc/src/display.rs new file mode 100644 index 0000000..3daa3eb --- /dev/null +++ b/src/uu/lsipc/src/display.rs @@ -0,0 +1,357 @@ +// 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. + +use std::borrow::Cow; +use std::ffi::{CStr, CString, c_uint}; +use std::io::{Cursor, Write}; +use std::ptr::NonNull; +use std::sync::atomic::AtomicPtr; +use std::{io, ptr}; + +use crate::column::ColumnInfo; +use crate::errors::LsIpcError; +use crate::smartcols::{IterDirection, Table, TableOperations, TableRef}; +use crate::utils::{UserDbRecordRef, find_replace_in_vec, local_time}; +use crate::{OutputMode, options}; + +fn decimal_point() -> &'static str { + use std::sync::atomic::Ordering; + + static DEFAULT: &CStr = c"."; + static VALUE: AtomicPtr = AtomicPtr::new(ptr::null_mut()); + + let mut decimal_point = VALUE.load(Ordering::Acquire); + + if decimal_point.is_null() { + decimal_point = unsafe { libc::localeconv().as_ref() } + .and_then(|lc| (!lc.decimal_point.is_null()).then_some(lc.decimal_point)) + .unwrap_or(DEFAULT.as_ptr().cast_mut()) + .cast(); + + match VALUE.compare_exchange( + ptr::null_mut(), + decimal_point, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_previous_value) => {} + Err(previous_value) => decimal_point = previous_value, + } + } + + unsafe { CStr::from_ptr(decimal_point.cast()) } + .to_str() + .unwrap() +} + +fn ascii_mode(mode: c_uint) -> [u8; 11] { + use libc::{ + S_IRGRP, S_IROTH, S_IRUSR, S_ISGID, S_ISUID, S_ISVTX, S_IWGRP, S_IWOTH, S_IWUSR, S_IXGRP, + S_IXOTH, S_IXUSR, + }; + + let mut buffer = Cursor::new([0_u8; 11]); + + match mode & libc::S_IFMT { + libc::S_IFDIR => write!(&mut buffer, "d"), + libc::S_IFLNK => write!(&mut buffer, "l"), + libc::S_IFCHR => write!(&mut buffer, "c"), + libc::S_IFBLK => write!(&mut buffer, "b"), + libc::S_IFSOCK => write!(&mut buffer, "s"), + libc::S_IFIFO => write!(&mut buffer, "p"), + libc::S_IFREG => write!(&mut buffer, "-"), + _ => Ok(()), + } + .unwrap(); + + for (readable, writable, executable, set_id_flag, set_id, set_id_without_x) in [ + (S_IRUSR, S_IWUSR, S_IXUSR, S_ISUID, 's', 'S'), + (S_IRGRP, S_IWGRP, S_IXGRP, S_ISGID, 's', 'S'), + (S_IROTH, S_IWOTH, S_IXOTH, S_ISVTX, 't', 'T'), + ] { + let r = if (mode & readable) == 0 { '-' } else { 'r' }; + let w = if (mode & writable) == 0 { '-' } else { 'w' }; + let x = match ((mode & set_id_flag) == 0, (mode & executable) == 0) { + (true, true) => '-', + (true, false) => 'x', + (false, true) => set_id_without_x, + (false, false) => set_id, + }; + write!(&mut buffer, "{r}{w}{x}").unwrap(); + } + + write!(&mut buffer, "\x00").unwrap(); + buffer.into_inner() +} + +fn strftime(format: &CStr, detailed_time: &libc::tm, buffer: &mut [u8]) -> Result<(), LsIpcError> { + buffer.fill(0_u8); + + errno::set_errno(errno::Errno(0)); + + let r = unsafe { + libc::strftime( + buffer.as_mut_ptr().cast(), + buffer.len(), + format.as_ptr(), + detailed_time, + ) + }; + + if r == 0 { + let err = io::Error::last_os_error(); + if err.raw_os_error().is_some_and(|n| n != 0) { + return Err(LsIpcError::io0("strftime", err)); + } + } + Ok(()) +} + +pub(crate) fn format_time( + format: crate::TimeFormat, + now: &libc::timeval, + time: libc::time_t, +) -> Result, LsIpcError> { + if time == 0 { + return Ok(None); + } + + let mut buffer: [u8; 256]; + + match format { + crate::TimeFormat::Short => { + let detailed_time = local_time(time)?; + let detailed_now = local_time(now.tv_sec)?; + + if detailed_time.tm_yday == detailed_now.tm_yday + && detailed_time.tm_year == detailed_now.tm_year + { + let result = format!("{:02}:{:02}", detailed_time.tm_hour, detailed_time.tm_min); + return Ok(Some(CString::new(result).unwrap())); + } else if detailed_time.tm_year == detailed_now.tm_year { + buffer = [0_u8; 256]; + strftime(c"%b%d", &detailed_time, &mut buffer)?; + } else { + buffer = [0_u8; 256]; + strftime(c"%Y-%b%d", &detailed_time, &mut buffer)?; + } + } + + crate::TimeFormat::Full => { + let detailed_time = local_time(time)?; + + buffer = [0_u8; 256]; + if unsafe { libc::asctime_r(&detailed_time, buffer.as_mut_ptr().cast()) }.is_null() { + return Err(LsIpcError::last_io0("asctime_r")); + } + + find_replace_in_vec(b'\n', 0_u8, &mut buffer); + } + + crate::TimeFormat::Iso => { + let detailed_time = local_time(time)?; + + let tz_minutes = if detailed_time.tm_isdst < 0 { + 0 + } else { + detailed_time.tm_gmtoff / 60 + }; + + let tz_hours = tz_minutes / 60; + let tz_minutes = (tz_minutes % 60).abs(); + + buffer = [0_u8; 256]; + let mut cursor = Cursor::new(buffer); + + write!( + &mut cursor, + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}{tz_hours:+03}:{tz_minutes:02}", + detailed_time.tm_year + 1900, + detailed_time.tm_mon + 1, + detailed_time.tm_mday, + detailed_time.tm_hour, + detailed_time.tm_min, + detailed_time.tm_sec, + ) + .map_err(|err| LsIpcError::io0("failed to write data", err))?; + + buffer = cursor.into_inner(); + } + } + + *buffer.last_mut().unwrap() = 0_u8; + let result = CStr::from_bytes_until_nul(&buffer).unwrap(); + Ok(Some(CString::from(result))) +} + +// returns exponent (2^x=n) in range KiB..EiB (2^10..2^60). +fn bytes_exponent(bytes: u64) -> u64 { + for shift in (10..=60).step_by(10) { + if bytes < (1 << shift) { + return shift - 10; + } + } + 60 +} + +fn size_to_human_string(bytes: u64) -> String { + static LETTERS: [char; 7] = ['B', 'K', 'M', 'G', 'T', 'P', 'E']; + + let exp = bytes_exponent(bytes); + let unit = LETTERS[if exp == 0 { 0 } else { (exp / 10) as usize }]; + let mut decimal = if exp == 0 { bytes } else { bytes / (1 << exp) }; + let mut fractional = if exp == 0 { 0 } else { bytes % (1 << exp) }; + + if fractional != 0 { + fractional = if fractional >= (u64::MAX / 1000) { + ((fractional / 1024) * 1000) / (1 << (exp - 10)) + } else { + (fractional * 1000) / (1 << exp) + }; + + fractional = ((fractional + 50) / 100) * 10; + + if fractional == 100 { + decimal += 1; + fractional = 0; + } + } + + if fractional == 0 { + format!("{decimal}{unit}") + } else { + format!("{decimal}{}{fractional:02}{unit}", decimal_point()) + } +} + +fn size_desc(size: u64, in_bytes: bool) -> String { + if in_bytes { + size.to_string() + } else { + size_to_human_string(size) + } +} + +pub(crate) fn describe_key(key: libc::key_t) -> Option> { + Some(Cow::Owned(CString::new(format!("{key:#010x}")).unwrap())) +} + +pub(crate) fn describe_integer(n: T) -> Option> { + Some(Cow::Owned(CString::new(n.to_string()).unwrap())) +} + +pub(crate) fn describe_size(size: u64, in_bytes: bool) -> Option> { + Some(Cow::Owned(CString::new(size_desc(size, in_bytes)).unwrap())) +} + +pub(crate) fn describe_owner(users: &mut UserDbRecordRef, uid: libc::uid_t) -> Option> { + if let Some(name) = users.for_id(uid).name() { + Some(Cow::Borrowed(name)) + } else { + Some(Cow::Owned(CString::new(uid.to_string()).unwrap())) + } +} + +pub(crate) fn describe_permissions( + args: &clap::ArgMatches, + permissions: c_uint, +) -> Option> { + if args.get_flag(options::NUMERIC_PERMS) { + let s = format!("{permissions:04o}"); + Some(Cow::Owned(CString::new(s).unwrap())) + } else { + let s = ascii_mode(permissions); + let s = CStr::from_bytes_until_nul(&s).unwrap(); + Some(Cow::Owned(s.into())) + } +} + +pub(crate) fn new_global_line( + columns: &[&ColumnInfo], + table: &mut Table, + resource: &CStr, + description: &CStr, + used: Option, + limit: u64, + in_bytes: bool, +) -> Result<(), LsIpcError> { + let mut line = table.new_line(None)?; + + for (cell_index, &column) in columns.iter().enumerate() { + let data_str = match column.id.to_bytes() { + b"RESOURCE" => Cow::Borrowed(resource), + b"DESCRIPTION" => Cow::Borrowed(description), + + b"LIMIT" => Cow::Owned(CString::new(size_desc(limit, in_bytes)).unwrap()), + + b"USED" => used.map_or(Cow::Borrowed(c"-"), |used| { + Cow::Owned(CString::new(size_desc(used, in_bytes)).unwrap()) + }), + + b"USE%" => used.map_or(Cow::Borrowed(c"-"), |used| { + let percent = (used as f64) / (limit as f64) * 100.0; + Cow::Owned(CString::new(format!("{percent:2.2}%")).unwrap()) + }), + + _ => continue, + }; + + line.set_data(cell_index, &data_str)?; + } + Ok(()) +} + +pub(crate) fn new_table( + args: &clap::ArgMatches, + output_mode: OutputMode, +) -> Result { + let mut table = Table::new()?; + + if args.get_flag(options::NO_HEADINGS) { + table.enable_headings(false)?; + } + + if args.get_flag(options::SHELL) { + table.enable_shell_variable(true)?; + } + + match output_mode { + OutputMode::Export => table.enable_export(true)?, + OutputMode::NewLine => { + table.set_column_separator(c"\n")?; + table.enable_export(true)? + } + OutputMode::Raw => table.enable_raw(true)?, + OutputMode::Json => table.enable_json(true)?, + OutputMode::Pretty => table.enable_headings(false)?, + OutputMode::None | OutputMode::List => {} + } + + Ok(table) +} + +pub(crate) fn print_pretty_table(table: &Table) -> Result<(), LsIpcError> { + let line = table.line(0)?; + + for (cell_index, _column) in table.column_iter(IterDirection::Forward)?.enumerate() { + if let Some(dstr) = line + .cell(cell_index)? + .data_as_c_str() + .filter(|&s| !s.is_empty()) + .map(CStr::to_bytes) + .and_then(|b| std::str::from_utf8(b).ok()) + { + let title = crate::column::COLUMN_INFOS[cell_index].title; + println!("{title}:{}{dstr: <36}", " ".repeat(35 - title.len())); + } + } + + if let Some(sub_table) = NonNull::new(line.user_data().cast()).map(TableRef::from) { + println!("Elements:"); + println!(); + sub_table.print()?; + } + Ok(()) +} diff --git a/src/uu/lsipc/src/errors.rs b/src/uu/lsipc/src/errors.rs new file mode 100644 index 0000000..257bd10 --- /dev/null +++ b/src/uu/lsipc/src/errors.rs @@ -0,0 +1,74 @@ +// 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::c_int; +use std::fmt; +use std::path::PathBuf; + +use uucore::error::UError; + +#[derive(Debug)] +pub enum LsIpcError { + InvalidColumnName(String), + InvalidColumnSequence(String), + InvalidTimeFormat(String), + IO0(String, std::io::Error), + IO1(String, PathBuf, std::io::Error), +} + +impl LsIpcError { + pub(crate) fn io0(message: impl Into, error: impl Into) -> Self { + Self::IO0(message.into(), error.into()) + } + + pub(crate) fn last_io0(message: impl Into) -> Self { + let err = std::io::Error::last_os_error(); + Self::IO0(message.into(), err) + } + + pub(crate) fn io1( + message: impl Into, + path: impl Into, + error: impl Into, + ) -> Self { + Self::IO1(message.into(), path.into(), error.into()) + } + + pub(crate) fn io_from_neg_errno( + message: impl Into, + result: c_int, + ) -> Result { + if let Ok(result) = usize::try_from(result) { + Ok(result) + } else { + let err = std::io::Error::from_raw_os_error(-result); + Err(Self::IO0(message.into(), err)) + } + } +} + +impl fmt::Display for LsIpcError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::IO0(message, err) => write!(f, "{message}: {err}"), + Self::IO1(message, path, err) => write!(f, "{message} '{}': {err}", path.display()), + Self::InvalidColumnName(name) => write!(f, "invalid column name: {name}"), + Self::InvalidColumnSequence(seq) => write!(f, "invalid column sequence: {seq}"), + Self::InvalidTimeFormat(mode) => write!(f, "invalid time format: {mode}"), + } + } +} + +impl UError for LsIpcError { + fn code(&self) -> i32 { + 1 + } + + fn usage(&self) -> bool { + false + } +} + +impl std::error::Error for LsIpcError {} diff --git a/src/uu/lsipc/src/lsipc.rs b/src/uu/lsipc/src/lsipc.rs new file mode 100644 index 0000000..7a32f79 --- /dev/null +++ b/src/uu/lsipc/src/lsipc.rs @@ -0,0 +1,425 @@ +// 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(target_os = "linux"), allow(dead_code))] + +#[cfg(target_os = "linux")] +mod column; +#[cfg(target_os = "linux")] +mod display; +mod errors; +#[cfg(target_os = "linux")] +mod message_queue; +#[cfg(target_os = "linux")] +mod semaphore; +#[cfg(target_os = "linux")] +mod shared_memory; +#[cfg(target_os = "linux")] +mod smartcols; +#[cfg(target_os = "linux")] +mod utils; + +use std::ffi::c_uint; +use std::str::FromStr; + +use clap::builder::{EnumValueParser, PossibleValue}; +use clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command, ValueEnum, crate_version, value_parser}; +use uucore::{error::UResult, format_usage, help_about, help_usage}; + +#[cfg(target_os = "linux")] +use crate::column::{ColumnInfo, OutputColumns}; +use crate::errors::LsIpcError; +#[cfg(target_os = "linux")] +use crate::smartcols::TableOperations; + +mod options { + pub static BYTES: &str = "bytes"; + pub static CREATOR: &str = "creator"; + pub static EXPORT: &str = "export"; + pub static GLOBAL: &str = "global"; + pub static ID: &str = "id"; + pub static JSON: &str = "json"; + pub static LIST: &str = "list"; + pub static NEW_LINE: &str = "newline"; + pub static NO_HEADINGS: &str = "noheadings"; + pub static NO_TRUNCATE: &str = "notruncate"; + pub static NUMERIC_PERMS: &str = "numeric-perms"; + pub static OUTPUT: &str = "output"; + pub static QUEUES: &str = "queues"; + pub static RAW: &str = "raw"; + pub static SEMAPHORES: &str = "semaphores"; + pub static SHELL: &str = "shell"; + pub static SHMEMS: &str = "shmems"; + pub static TIME_FORMAT: &str = "time-format"; + pub static TIME: &str = "time"; + pub static TYPE: &str = "type"; + pub const SHORT: &str = "short"; + pub const FULL: &str = "full"; + pub const ISO: &str = "iso"; +} + +const ABOUT: &str = help_about!("lsipc.md"); +const USAGE: &str = help_usage!("lsipc.md"); + +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( + Arg::new(options::BYTES) + .short('b') + .long(options::BYTES) + .action(ArgAction::SetTrue) + .help("print SIZE in bytes rather than in human readable format"), + ) + .arg( + Arg::new(options::CREATOR) + .short('c') + .long(options::CREATOR) + .action(ArgAction::SetTrue) + .requires("ipc-kind") + .help("show creator and owner"), + ) + .arg( + Arg::new(options::EXPORT) + .short('e') + .long(options::EXPORT) + .action(ArgAction::SetTrue) + .help("display in an export-able output format"), + ) + .arg( + Arg::new(options::GLOBAL) + .short('g') + .long(options::GLOBAL) + .action(ArgAction::SetTrue) + .conflicts_with(options::ID) + .help("info about system-wide usage"), + ) + .arg( + Arg::new(options::ID) + .short('i') + .long(options::ID) + .value_name(options::ID) + .value_parser(value_parser!(c_uint)) + .action(ArgAction::Set) + .conflicts_with(options::GLOBAL) + .requires("ipc-kind") + .help("print details on resource identified by id"), + ) + .arg( + Arg::new(options::JSON) + .short('J') + .long(options::JSON) + .action(ArgAction::SetTrue) + .help("use the JSON output format"), + ) + .arg( + Arg::new(options::LIST) + .short('l') + .long(options::LIST) + .action(ArgAction::SetTrue) + .help("force list output format"), + ) + .arg( + Arg::new(options::NEW_LINE) + .short('n') + .long(options::NEW_LINE) + .action(ArgAction::SetTrue) + .help("display each piece of information on a new line"), + ) + .arg( + Arg::new(options::NO_HEADINGS) + .long(options::NO_HEADINGS) + .action(ArgAction::SetTrue) + .help("don't print headings"), + ) + .arg( + Arg::new(options::NO_TRUNCATE) + .long(options::NO_TRUNCATE) + .action(ArgAction::SetTrue) + .help("don't truncate output"), + ) + .arg( + Arg::new(options::NUMERIC_PERMS) + .short('P') + .long(options::NUMERIC_PERMS) + .action(ArgAction::SetTrue) + .help("print numeric permissions"), + ) + .arg( + Arg::new(options::OUTPUT) + .short('o') + .long(options::OUTPUT) + .value_name(options::LIST) + .value_parser(OutputColumns::from_str) + .action(ArgAction::Set) + .help("define the columns to output"), + ) + .arg( + Arg::new(options::QUEUES) + .short('q') + .long(options::QUEUES) + .action(ArgAction::SetTrue) + .help("message queues"), + ) + .arg( + Arg::new(options::RAW) + .short('r') + .long(options::RAW) + .action(ArgAction::SetTrue) + .help("display in raw mode"), + ) + .arg( + Arg::new(options::SEMAPHORES) + .short('s') + .long(options::SEMAPHORES) + .action(ArgAction::SetTrue) + .help("semaphores"), + ) + .arg( + Arg::new(options::SHELL) + .short('y') + .long(options::SHELL) + .action(ArgAction::SetTrue) + .help("use column names to be usable as shell variable identifiers"), + ) + .arg( + Arg::new(options::SHMEMS) + .short('m') + .long(options::SHMEMS) + .action(ArgAction::SetTrue) + .help("shared memory segments"), + ) + .arg( + Arg::new(options::TIME_FORMAT) + .long(options::TIME_FORMAT) + .value_name(options::TYPE) + .value_parser(EnumValueParser::::new()) + .action(ArgAction::Set) + .help("display dates in short, full or iso format"), + ) + .arg( + Arg::new(options::TIME) + .short('t') + .long(options::TIME) + .action(ArgAction::SetTrue) + .requires("ipc-kind") + .help("show attach, detach and change times"), + ) + .group( + ArgGroup::new("ipc-kind") + .args([options::SHMEMS, options::QUEUES, options::SEMAPHORES]) + .multiple(false), + ) + .group( + ArgGroup::new("output-kind") + .args([ + options::EXPORT, + options::JSON, + options::LIST, + options::NEW_LINE, + options::RAW, + ]) + .multiple(false), + ) + .after_help(include_str!("../after-help.txt")) +} + +#[uucore::main] +pub fn uumain(args: impl uucore::Args) -> UResult<()> { + let args = uu_app().try_get_matches_from_mut(args)?; + + let output_mode = OutputMode::from(&args); + + let time_format = args + .get_one::(options::TIME_FORMAT) + .copied() + .unwrap_or(if output_mode == OutputMode::Pretty { + TimeFormat::Full + } else { + TimeFormat::Short + }); + + lsipc(&args, output_mode, time_format).map_err(From::from) +} + +// The Linux implementation resides in `crate::column`. +#[cfg(not(target_os = "linux"))] +#[derive(Debug, Clone)] +struct OutputColumns; + +#[cfg(not(target_os = "linux"))] +impl FromStr for OutputColumns { + type Err = LsIpcError; + + fn from_str(_s: &str) -> Result { + unimplemented!() + } +} + +#[cfg(target_os = "linux")] +fn lsipc( + args: &ArgMatches, + output_mode: OutputMode, + time_format: TimeFormat, +) -> Result<(), LsIpcError> { + let columns = OutputColumns::from(args); + + let columns = if columns.append { + let mut default_columns = if output_mode == OutputMode::Pretty + && !args.get_flag(options::CREATOR) + && !args.get_flag(options::TIME) + { + column::all_defaults(args)? + } else { + column::filter_defaults(args)? + }; + + default_columns.extend(columns.list); + default_columns + } else { + columns.list + }; + + smartcols::initialize(); + + let mut table = display::new_table(args, output_mode)?; + + let no_truncate = args.get_flag(options::NO_TRUNCATE); + for &column in &columns { + let mut flags = column.flags; + if no_truncate { + flags &= !smartcols_sys::SCOLS_FL_TRUNC; + } + table.new_column(column.id, column.width_hint, flags)?; + } + + let print_global = args.get_flag(options::GLOBAL); + + if print_global { + table.set_name(c"ipclimits")?; + } + + type GlobalProc = + fn(&ArgMatches, &[&ColumnInfo], &mut smartcols::Table) -> Result<(), LsIpcError>; + + type DescribeProc = fn( + &ArgMatches, + TimeFormat, + &[&ColumnInfo], + &mut smartcols::Table, + Option, + ) -> Result<(), LsIpcError>; + + let queues = args.get_flag(options::QUEUES); + let shmems = args.get_flag(options::SHMEMS); + let semaphores = args.get_flag(options::SEMAPHORES); + + let config_list: [(bool, GlobalProc, DescribeProc); 3] = [ + (queues, message_queue::print_global, message_queue::describe), + (shmems, shared_memory::print_global, shared_memory::describe), + (semaphores, semaphore::print_global, semaphore::describe), + ]; + + let id = args.get_one::(options::ID).copied(); + + for (flag, global, describe) in config_list { + if flag || (!queues && !shmems && !semaphores) { + if print_global { + global(args, &columns, &mut table)?; + } else { + describe(args, time_format, &columns, &mut table, id)?; + } + } + } + + if output_mode == OutputMode::Pretty { + display::print_pretty_table(&table)?; + } else { + table.print()?; + } + + Ok(()) +} + +#[cfg(not(target_os = "linux"))] +fn lsipc( + _args: &ArgMatches, + _output_mode: OutputMode, + _time_format: TimeFormat, +) -> Result<(), LsIpcError> { + unimplemented!() +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[repr(u8)] +pub(crate) enum TimeFormat { + Short = 0, + Full, + Iso, +} + +impl FromStr for TimeFormat { + type Err = LsIpcError; + + fn from_str(s: &str) -> Result { + match s { + options::SHORT => Ok(Self::Short), + options::FULL => Ok(Self::Full), + options::ISO => Ok(Self::Iso), + _ => Err(LsIpcError::InvalidTimeFormat(s.into())), + } + } +} + +impl ValueEnum for TimeFormat { + fn value_variants<'a>() -> &'a [Self] { + &[Self::Short, Self::Full, Self::Iso] + } + + fn to_possible_value<'a>(&self) -> Option { + let name = match self { + Self::Short => options::SHORT, + Self::Full => options::FULL, + Self::Iso => options::ISO, + }; + Some(PossibleValue::new(name)) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[repr(u8)] +pub(crate) enum OutputMode { + None = 0, + Export, + NewLine, + Raw, + Json, + Pretty, + List, +} + +impl From<&'_ ArgMatches> for OutputMode { + fn from(args: &'_ ArgMatches) -> Self { + if args.get_flag(options::EXPORT) { + Self::Export + } else if args.get_flag(options::JSON) { + Self::Json + } else if args.get_flag(options::LIST) { + Self::List + } else if args.get_flag(options::NEW_LINE) { + Self::NewLine + } else if args.get_flag(options::RAW) { + Self::Raw + } else if args.contains_id(options::ID) { + Self::Pretty + } else { + Self::None + } + } +} diff --git a/src/uu/lsipc/src/main.rs b/src/uu/lsipc/src/main.rs new file mode 100644 index 0000000..60344e3 --- /dev/null +++ b/src/uu/lsipc/src/main.rs @@ -0,0 +1 @@ +uucore::bin!(uu_lsipc); diff --git a/src/uu/lsipc/src/message_queue.rs b/src/uu/lsipc/src/message_queue.rs new file mode 100644 index 0000000..a06b3d0 --- /dev/null +++ b/src/uu/lsipc/src/message_queue.rs @@ -0,0 +1,390 @@ +// 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. + +use std::borrow::Cow; +use std::ffi::{c_int, c_uint}; +use std::fs::File; +use std::io; +use std::io::{BufRead, BufReader}; +use std::str::FromStr; + +use crate::column::ColumnInfo; +use crate::display::{ + describe_integer, describe_key, describe_owner, describe_permissions, describe_size, + format_time, new_global_line, +}; +use crate::errors::LsIpcError; +use crate::smartcols::{Table, TableOperations}; +use crate::utils::{GroupDbRecordRef, UserDbRecordRef, read_value, time_of_day}; + +static _PATH_PROC_SYSV_MSG: &str = "/proc/sysvipc/msg"; +static _PATH_PROC_IPC_MSGMNI: &str = "/proc/sys/kernel/msgmni"; +static _PATH_PROC_IPC_MSGMNB: &str = "/proc/sys/kernel/msgmnb"; +static _PATH_PROC_IPC_MSGMAX: &str = "/proc/sys/kernel/msgmax"; + +fn msgctl(id: c_int, cmd: c_int) -> Result<(c_int, libc::msqid_ds), LsIpcError> { + let mut stat: libc::msqid_ds; + let r = unsafe { + stat = std::mem::zeroed(); + libc::msgctl(id, cmd, &mut stat) + }; + + if r == -1 { + Err(LsIpcError::last_io0("msgctl")) + } else { + Ok((r, stat)) + } +} + +struct Limits { + msg_mni: u64, + msg_mnb: u64, + msg_max: u64, +} + +impl Limits { + fn new() -> Result { + Self::from_proc().or_else(|_| Self::from_syscall()) + } + + fn from_proc() -> Result { + Ok(Self { + msg_mni: read_value::(_PATH_PROC_IPC_MSGMNI)?, + msg_mnb: read_value::(_PATH_PROC_IPC_MSGMNB)?, + msg_max: read_value::(_PATH_PROC_IPC_MSGMAX)?, + }) + } + + fn from_syscall() -> Result { + let (_, stat) = msgctl(0, libc::IPC_INFO)?; + + let msg_info = unsafe { &*(&stat as *const libc::msqid_ds).cast::() }; + + let err_map = move |_| LsIpcError::io0("invalid data", io::ErrorKind::InvalidData); + + Ok(Self { + msg_mni: u64::try_from(msg_info.msgmni).map_err(err_map)?, + msg_mnb: u64::try_from(msg_info.msgmnb).map_err(err_map)?, + msg_max: u64::try_from(msg_info.msgmax).map_err(err_map)?, + }) + } +} + +struct SysVIpcEntry { + key: libc::key_t, + msqid: c_int, + perms: c_uint, + cbytes: u64, + qnum: libc::msgqnum_t, + lspid: libc::pid_t, + lrpid: libc::pid_t, + uid: libc::uid_t, + gid: libc::gid_t, + cuid: libc::uid_t, + cgid: libc::gid_t, + stime: libc::time_t, + rtime: libc::time_t, + ctime: libc::time_t, +} + +impl SysVIpcEntry { + fn from_msqid_ds(msqid: c_int, stat: &libc::msqid_ds) -> Self { + Self { + key: stat.msg_perm.__key, + msqid, + perms: c_uint::from(stat.msg_perm.mode), + cbytes: stat.__msg_cbytes, + qnum: stat.msg_qnum, + lspid: stat.msg_lspid, + lrpid: stat.msg_lrpid, + uid: stat.msg_perm.uid, + gid: stat.msg_perm.gid, + cuid: stat.msg_perm.cuid, + cgid: stat.msg_perm.cgid, + stime: stat.msg_stime, + rtime: stat.msg_rtime, + ctime: stat.msg_ctime, + } + } +} + +impl FromStr for SysVIpcEntry { + type Err = LsIpcError; + + fn from_str(line: &str) -> Result { + let err_map = || { + let err = io::ErrorKind::InvalidData; + LsIpcError::io1("invalid data", _PATH_PROC_SYSV_MSG, err) + }; + + let mut iter = line.split_ascii_whitespace(); + let key = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let msqid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let perms = iter + .next() + .and_then(|s| c_uint::from_str_radix(s, 8).ok()) + .ok_or_else(err_map)?; + let cbytes = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let qnum = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let lspid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let lrpid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let uid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let gid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let cuid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let cgid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let stime = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let rtime = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let ctime = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + + Ok(Self { + key, + msqid, + perms, + cbytes, + qnum, + lspid, + lrpid, + uid, + gid, + cuid, + cgid, + stime, + rtime, + ctime, + }) + } +} + +struct SysVIpc(Vec); + +impl SysVIpc { + fn new(id: Option) -> Result { + if let Ok(file) = File::open(_PATH_PROC_SYSV_MSG).map(BufReader::new) { + if let Some(id) = id { + Self::from_proc_by_id(id, file) + } else { + Self::from_proc(file) + } + } else if let Some(id) = id { + Self::from_syscall_by_id(id) + } else { + Self::from_syscall() + } + } + + fn from_proc(mut file: BufReader) -> Result { + let err_map = + move |err: std::io::Error| LsIpcError::io1("reading file", _PATH_PROC_SYSV_MSG, err); + + file.skip_until(b'\n').map_err(err_map)?; // Skip the header line. + + let mut list = Vec::default(); + let mut line = String::default(); + + loop { + line.clear(); + if file.read_line(&mut line).map_err(err_map)? == 0 { + break; + } + + let entry = line.parse()?; + list.push(entry); + } + Ok(Self(list)) + } + + fn from_proc_by_id(id: c_uint, mut file: BufReader) -> Result { + let err_map = + move |err: std::io::Error| LsIpcError::io1("reading file", _PATH_PROC_SYSV_MSG, err); + + file.skip_until(b'\n').map_err(err_map)?; // Skip the header line. + + let mut line = String::default(); + + loop { + line.clear(); + if file.read_line(&mut line).map_err(err_map)? == 0 { + break Ok(Self(Vec::default())); + } + + let msqid: c_uint = line + .split_ascii_whitespace() + .nth(1) + .and_then(|s| s.parse().ok()) + .ok_or_else(|| { + let err = io::ErrorKind::InvalidData; + LsIpcError::io1("invalid data", _PATH_PROC_SYSV_MSG, err) + })?; + + if msqid == id { + let entry = line.parse()?; + break Ok(Self(vec![entry])); + } + } + } + + fn from_syscall() -> Result { + let (max_id, _) = msgctl(0, libc::MSG_INFO)?; + + let list = (0..max_id) + .map(|id| msgctl(id, libc::MSG_STAT)) + .filter_map(Result::ok) + .map(|(msqid, stat)| SysVIpcEntry::from_msqid_ds(msqid, &stat)) + .collect(); + + Ok(Self(list)) + } + + fn from_syscall_by_id(id: c_uint) -> Result { + let (max_id, _) = msgctl(0, libc::MSG_INFO)?; + + let list = (0..max_id) + .map(|current_id| msgctl(current_id, libc::MSG_STAT)) + .filter_map(Result::ok) + .filter(|(msqid, _stat)| c_uint::try_from(*msqid).is_ok_and(|msqid| msqid == id)) + .map(|(msqid, stat)| SysVIpcEntry::from_msqid_ds(msqid, &stat)) + .next() + .map_or_else(Vec::default, |entry| vec![entry]); + + Ok(Self(list)) + } +} + +pub(crate) fn print_global( + args: &clap::ArgMatches, + columns: &[&ColumnInfo], + table: &mut Table, +) -> Result<(), LsIpcError> { + let limits = Limits::new()?; + let count = SysVIpc::new(None)?.0.len(); + let in_bytes = args.get_flag(crate::options::BYTES); + + let lines_config = [ + ( + c"MSGMNI", + c"Number of message queues", + Some(u64::try_from(count).unwrap_or(u64::MAX)), + limits.msg_mni, + true, + ), + ( + c"MSGMAX", + c"Max size of message (bytes)", + None, + limits.msg_max, + in_bytes, + ), + ( + c"MSGMNB", + c"Default max size of queue (bytes)", + None, + limits.msg_mnb, + in_bytes, + ), + ]; + + lines_config + .into_iter() + .try_for_each(move |(resource, description, used, limit, in_bytes)| { + new_global_line(columns, table, resource, description, used, limit, in_bytes) + }) +} + +pub(crate) fn describe( + args: &clap::ArgMatches, + time_format: crate::TimeFormat, + columns: &[&ColumnInfo], + table: &mut Table, + id: Option, +) -> Result<(), LsIpcError> { + let now = time_of_day()?; + let sys_v_ipc = SysVIpc::new(id)?; + + if let Some(id) = id { + if sys_v_ipc.0.len() != 1 { + eprintln!("id {id} not found"); + return Ok(()); + } + } + + table.set_name(c"messages")?; + + let mut users = UserDbRecordRef::default(); + let mut groups = GroupDbRecordRef::default(); + + for entry in sys_v_ipc.0 { + let mut line = table.new_line(None)?; + + for (cell_index, &column) in columns.iter().enumerate() { + let data_str = match column.id.to_bytes() { + b"KEY" => describe_key(entry.key), + b"ID" => describe_integer(entry.msqid), + b"CUID" => describe_integer(entry.cuid), + b"CGID" => describe_integer(entry.cgid), + b"UID" => describe_integer(entry.uid), + b"GID" => describe_integer(entry.gid), + b"LSPID" => describe_integer(entry.lspid), + b"LRPID" => describe_integer(entry.lrpid), + b"MSGS" => describe_integer(entry.qnum), + b"CTIME" => format_time(time_format, &now, entry.ctime)?.map(Cow::Owned), + b"SEND" => format_time(time_format, &now, entry.stime)?.map(Cow::Owned), + b"RECV" => format_time(time_format, &now, entry.rtime)?.map(Cow::Owned), + b"USEDBYTES" => describe_size(entry.cbytes, args.get_flag(crate::options::BYTES)), + b"OWNER" => describe_owner(&mut users, entry.uid), + b"CUSER" => users.for_id(entry.cuid).name().map(Cow::Borrowed), + b"USER" => users.for_id(entry.uid).name().map(Cow::Borrowed), + b"CGROUP" => groups.for_id(entry.cgid).name().map(Cow::Borrowed), + b"GROUP" => groups.for_id(entry.gid).name().map(Cow::Borrowed), + b"PERMS" => describe_permissions(args, entry.perms & 0o777), + + _ => continue, + }; + + if let Some(data_str) = data_str { + line.set_data(cell_index, &data_str)?; + } + } + } + Ok(()) +} diff --git a/src/uu/lsipc/src/semaphore.rs b/src/uu/lsipc/src/semaphore.rs new file mode 100644 index 0000000..2eb59b4 --- /dev/null +++ b/src/uu/lsipc/src/semaphore.rs @@ -0,0 +1,493 @@ +// 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. + +use std::borrow::Cow; +use std::ffi::{CStr, CString, c_int, c_uint}; +use std::fs::File; +use std::io; +use std::io::{BufRead, BufReader}; +use std::str::FromStr; + +use crate::column::ColumnInfo; +use crate::display::{ + describe_integer, describe_key, describe_owner, describe_permissions, format_time, + new_global_line, +}; +use crate::errors::LsIpcError; +use crate::smartcols::{LineRef, Table, TableOperations}; +use crate::utils::{GroupDbRecordRef, UserDbRecordRef, pid_command_line, time_of_day}; + +const SEMVMX: u64 = 0x7fff; + +static _PATH_PROC_SYSV_SEM: &str = "/proc/sysvipc/sem"; +static _PATH_PROC_IPC_SEM: &str = "/proc/sys/kernel/sem"; + +fn semctl(id: c_int, index: c_int, cmd: c_int) -> Result<(c_int, libc::semid_ds), LsIpcError> { + let mut stat: libc::semid_ds; + let r = unsafe { + stat = std::mem::zeroed(); + libc::semctl(id, index, cmd, &mut stat) + }; + + if r == -1 { + Err(LsIpcError::last_io0("semctl")) + } else { + Ok((r, stat)) + } +} + +struct Limits { + sem_vmx: u64, + sem_mni: u64, + sem_msl: u64, + sem_mns: u64, + sem_opm: u64, +} + +impl Limits { + fn new() -> Result { + if let Ok(file) = File::open(_PATH_PROC_IPC_SEM).map(BufReader::new) { + Self::from_proc(file) + } else { + Self::from_syscall() + } + } + + fn from_proc(mut file: BufReader) -> Result { + let mut line = String::default(); + let count = file + .read_line(&mut line) + .map_err(|err| LsIpcError::io1("reading file", _PATH_PROC_IPC_SEM, err))?; + + if count == 0 { + let err = io::ErrorKind::UnexpectedEof; + Err(LsIpcError::io1("reading file", _PATH_PROC_IPC_SEM, err)) + } else { + line.parse() + } + } + + fn from_syscall() -> Result { + let (_, stat) = semctl(0, 0, libc::IPC_INFO)?; + + let sem_info = unsafe { &*(&stat as *const libc::semid_ds).cast::() }; + + let err_map = move |_| LsIpcError::io0("invalid data", io::ErrorKind::InvalidData); + + Ok(Self { + sem_vmx: u64::try_from(sem_info.semvmx).map_err(err_map)?, + sem_mni: u64::try_from(sem_info.semmni).map_err(err_map)?, + sem_msl: u64::try_from(sem_info.semmsl).map_err(err_map)?, + sem_mns: u64::try_from(sem_info.semmns).map_err(err_map)?, + sem_opm: u64::try_from(sem_info.semopm).map_err(err_map)?, + }) + } +} + +impl FromStr for Limits { + type Err = LsIpcError; + + fn from_str(line: &str) -> Result { + let err_map = || { + let err = io::ErrorKind::InvalidData; + LsIpcError::io1("invalid data", _PATH_PROC_IPC_SEM, err) + }; + + let mut iter = line.split_ascii_whitespace(); + let sem_msl = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let sem_mns = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let sem_opm = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let sem_mni = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + + Ok(Self { + sem_vmx: SEMVMX, + sem_mni, + sem_msl, + sem_mns, + sem_opm, + }) + } +} + +struct SysVIpcEntryElement { + sem_val: c_int, + ncount: u64, + zcount: u64, + pid: libc::pid_t, +} + +impl SysVIpcEntryElement { + fn new(semid: c_int, index: usize) -> Result { + let (sem_val, _) = semctl(semid, index as c_int, libc::GETVAL)?; + let (ncount, _) = semctl(semid, index as c_int, libc::GETNCNT)?; + let (zcount, _) = semctl(semid, index as c_int, libc::GETZCNT)?; + let (pid, _) = semctl(semid, index as c_int, libc::GETPID)?; + + let err_map = move |_| LsIpcError::io0("invalid data", io::ErrorKind::InvalidData); + + Ok(Self { + sem_val, + ncount: u64::try_from(ncount).map_err(err_map)?, + zcount: u64::try_from(zcount).map_err(err_map)?, + pid, + }) + } +} + +fn semaphore_elements(semid: c_int, nsems: u64) -> Result, LsIpcError> { + (0..usize::try_from(nsems).unwrap_or(usize::MAX)) + .map(move |index| SysVIpcEntryElement::new(semid, index)) + .collect() +} + +struct SysVIpcEntry { + key: libc::key_t, + semid: c_int, + perms: c_uint, + uid: libc::uid_t, + gid: libc::gid_t, + cuid: libc::uid_t, + cgid: libc::gid_t, + otime: libc::time_t, + ctime: libc::time_t, + elements: Vec, +} + +impl SysVIpcEntry { + fn from_semid_ds( + semid: c_int, + stat: &libc::semid_ds, + elements: Vec, + ) -> Self { + Self { + key: stat.sem_perm.__key, + semid, + perms: c_uint::from(stat.sem_perm.mode), + uid: stat.sem_perm.uid, + gid: stat.sem_perm.gid, + cuid: stat.sem_perm.cuid, + cgid: stat.sem_perm.cgid, + otime: stat.sem_otime, + ctime: stat.sem_ctime, + elements, + } + } +} + +impl FromStr for SysVIpcEntry { + type Err = LsIpcError; + + fn from_str(line: &str) -> Result { + let err_map = || { + let err = io::ErrorKind::InvalidData; + LsIpcError::io1("invalid data", _PATH_PROC_SYSV_SEM, err) + }; + + let mut iter = line.split_ascii_whitespace(); + let key = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let semid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let perms = iter + .next() + .and_then(|s| c_uint::from_str_radix(s, 8).ok()) + .ok_or_else(err_map)?; + let nsems = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let uid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let gid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let cuid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let cgid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let otime = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let ctime = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + + let elements = semaphore_elements(semid, nsems)?; + + Ok(Self { + key, + semid, + perms, + uid, + gid, + cuid, + cgid, + otime, + ctime, + elements, + }) + } +} + +struct SysVIpc(Vec); + +impl SysVIpc { + fn new(id: Option) -> Result { + if let Ok(file) = File::open(_PATH_PROC_SYSV_SEM).map(BufReader::new) { + if let Some(id) = id { + Self::from_proc_by_id(id, file) + } else { + Self::from_proc(file) + } + } else if let Some(id) = id { + Self::from_syscall_by_id(id) + } else { + Self::from_syscall() + } + } + + fn from_proc(mut file: BufReader) -> Result { + let err_map = + move |err: std::io::Error| LsIpcError::io1("reading file", _PATH_PROC_SYSV_SEM, err); + + file.skip_until(b'\n').map_err(err_map)?; // Skip the header line. + + let mut list = Vec::default(); + let mut line = String::default(); + + loop { + line.clear(); + if file.read_line(&mut line).map_err(err_map)? == 0 { + break; + } + + let entry = line.parse()?; + list.push(entry); + } + Ok(Self(list)) + } + + fn from_proc_by_id(id: c_uint, mut file: BufReader) -> Result { + let err_map = + move |err: std::io::Error| LsIpcError::io1("reading file", _PATH_PROC_SYSV_SEM, err); + + file.skip_until(b'\n').map_err(err_map)?; // Skip the header line. + + let mut line = String::default(); + + loop { + line.clear(); + if file.read_line(&mut line).map_err(err_map)? == 0 { + break Ok(Self(Vec::default())); + } + + let semid: c_uint = line + .split_ascii_whitespace() + .nth(1) + .and_then(|s| s.parse().ok()) + .ok_or_else(|| { + let err = io::ErrorKind::InvalidData; + LsIpcError::io1("invalid data", _PATH_PROC_SYSV_SEM, err) + })?; + + if semid == id { + let entry = line.parse()?; + break Ok(Self(vec![entry])); + } + } + } + + fn from_syscall() -> Result { + let (max_id, _) = semctl(0, 0, libc::SEM_INFO)?; + + let list = (0..max_id) + .filter_map(|id| semctl(id, 0, libc::SEM_STAT).ok()) + .filter_map(|(semid, stat)| { + let elements = semaphore_elements(semid, stat.sem_nsems).ok()?; + Some(SysVIpcEntry::from_semid_ds(semid, &stat, elements)) + }) + .collect(); + + Ok(Self(list)) + } + + fn from_syscall_by_id(id: c_uint) -> Result { + let (max_id, _) = semctl(0, 0, libc::SEM_INFO)?; + + let list = (0..max_id) + .map(|current_id| semctl(current_id, 0, libc::SEM_STAT)) + .filter_map(Result::ok) + .filter(|(semid, _stat)| c_uint::try_from(*semid).is_ok_and(|semid| semid == id)) + .filter_map(|(semid, stat)| { + let elements = semaphore_elements(semid, stat.sem_nsems).ok()?; + Some(SysVIpcEntry::from_semid_ds(semid, &stat, elements)) + }) + .next() + .map_or_else(Vec::default, |entry| vec![entry]); + + Ok(Self(list)) + } +} + +pub(crate) fn print_global( + _args: &clap::ArgMatches, + columns: &[&ColumnInfo], + table: &mut Table, +) -> Result<(), LsIpcError> { + let limits = Limits::new()?; + let sys_v_ipc = SysVIpc::new(None)?; + let total_nsems: usize = sys_v_ipc.0.iter().map(|sem| sem.elements.len()).sum(); + + let lines_config = [ + ( + c"SEMMNI", + c"Number of semaphore identifiers", + Some(u64::try_from(sys_v_ipc.0.len()).unwrap_or(u64::MAX)), + limits.sem_mni, + ), + ( + c"SEMMNS", + c"Total number of semaphores", + Some(u64::try_from(total_nsems).unwrap_or(u64::MAX)), + limits.sem_mns, + ), + ( + c"SEMMSL", + c"Max semaphores per semaphore set", + None, + limits.sem_msl, + ), + ( + c"SEMOPM", + c"Max number of operations per semop(2)", + None, + limits.sem_opm, + ), + (c"SEMVMX", c"Semaphore max value", None, limits.sem_vmx), + ]; + + lines_config + .into_iter() + .try_for_each(move |(resource, description, used, limit)| { + new_global_line(columns, table, resource, description, used, limit, true) + }) +} + +pub(crate) fn describe( + args: &clap::ArgMatches, + time_format: crate::TimeFormat, + columns: &[&ColumnInfo], + table: &mut Table, + id: Option, +) -> Result<(), LsIpcError> { + let now = time_of_day()?; + let sys_v_ipc = SysVIpc::new(id)?; + + if let Some(id) = id { + if sys_v_ipc.0.len() != 1 { + eprintln!("id {id} not found"); + return Ok(()); + } + } + + table.set_name(c"semaphores")?; + + let mut users = UserDbRecordRef::default(); + let mut groups = GroupDbRecordRef::default(); + + for entry in sys_v_ipc.0 { + let mut line = table.new_line(None)?; + + for (cell_index, &column) in columns.iter().enumerate() { + let data_str = match column.id.to_bytes() { + b"KEY" => describe_key(entry.key), + b"ID" => describe_integer(entry.semid), + b"CUID" => describe_integer(entry.cuid), + b"CGID" => describe_integer(entry.cgid), + b"UID" => describe_integer(entry.uid), + b"GID" => describe_integer(entry.gid), + b"NSEMS" => describe_integer(entry.elements.len()), + b"CTIME" => format_time(time_format, &now, entry.ctime)?.map(Cow::Owned), + b"OTIME" => format_time(time_format, &now, entry.otime)?.map(Cow::Owned), + b"OWNER" => describe_owner(&mut users, entry.uid), + b"CUSER" => users.for_id(entry.cuid).name().map(Cow::Borrowed), + b"USER" => users.for_id(entry.uid).name().map(Cow::Borrowed), + b"CGROUP" => groups.for_id(entry.cgid).name().map(Cow::Borrowed), + b"GROUP" => groups.for_id(entry.gid).name().map(Cow::Borrowed), + b"PERMS" => describe_permissions(args, entry.perms & 0o777), + + _ => continue, + }; + + if let Some(data_str) = data_str { + line.set_data(cell_index, &data_str)?; + } + } + + if id.is_some() && !entry.elements.is_empty() { + describe_elements(&mut line, &entry.elements)?; + } + } + Ok(()) +} + +fn describe_elements( + line: &mut LineRef, + elements: &[SysVIpcEntryElement], +) -> Result<(), LsIpcError> { + static ELEMENT_COLUMNS: [&CStr; 6] = [ + c"SEMNUM", c"VALUE", c"NCOUNT", c"ZCOUNT", c"PID", c"COMMAND", + ]; + + let mut sub_table = Table::new()?; + sub_table.set_name(c"elements")?; + sub_table.enable_headings(true)?; + + ELEMENT_COLUMNS.into_iter().try_for_each(|name| { + sub_table.new_column(name, 0.0, smartcols_sys::SCOLS_FL_RIGHT)?; + Ok(()) + })?; + + for (index, element) in elements.iter().enumerate() { + let mut sub_line = sub_table.new_line(None)?; + + sub_line.set_data(0, &CString::new(index.to_string()).unwrap())?; + sub_line.set_data(1, &CString::new(element.sem_val.to_string()).unwrap())?; + sub_line.set_data(2, &CString::new(element.ncount.to_string()).unwrap())?; + sub_line.set_data(3, &CString::new(element.zcount.to_string()).unwrap())?; + sub_line.set_data(4, &CString::new(element.pid.to_string()).unwrap())?; + + if let Ok(cmd_line) = pid_command_line(element.pid) { + sub_line.set_data(5, &cmd_line)?; + } + } + + line.set_user_data(sub_table.into_inner().as_ptr().cast()) +} diff --git a/src/uu/lsipc/src/shared_memory.rs b/src/uu/lsipc/src/shared_memory.rs new file mode 100644 index 0000000..cd44d84 --- /dev/null +++ b/src/uu/lsipc/src/shared_memory.rs @@ -0,0 +1,442 @@ +// 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. + +use std::borrow::Cow; +use std::ffi::{CString, c_int, c_uint, c_ulong}; +use std::fs::File; +use std::io; +use std::io::{BufRead, BufReader}; +use std::str::FromStr; + +use crate::column::ColumnInfo; +use crate::display::{ + describe_integer, describe_key, describe_owner, describe_permissions, describe_size, + format_time, new_global_line, +}; +use crate::errors::LsIpcError; +use crate::smartcols::{Table, TableOperations}; +use crate::utils::{ + GroupDbRecordRef, UserDbRecordRef, get_page_size, pid_command_line, read_value, time_of_day, +}; + +const SHM_STAT: c_int = 13 | (libc::IPC_STAT & 0x100); +const SHM_INFO: c_int = 14; +const SHM_DEST: c_uint = 0o1000; +const SHM_LOCKED: c_uint = 0o2000; + +static _PATH_PROC_SYSV_SHM: &str = "/proc/sysvipc/shm"; +static _PATH_PROC_IPC_SHMMAX: &str = "/proc/sys/kernel/shmmax"; +static _PATH_PROC_IPC_SHMMNI: &str = "/proc/sys/kernel/shmmni"; +static _PATH_PROC_IPC_SHMALL: &str = "/proc/sys/kernel/shmall"; + +// The C struct is defined in +#[allow(non_camel_case_types)] +#[repr(C)] +struct shminfo { + shmmax: c_ulong, + shmmin: c_ulong, + shmmni: c_ulong, + shmseg: c_ulong, + shmall: c_ulong, + __unused: [c_ulong; 4], +} + +fn shmctl(id: c_int, cmd: c_int) -> Result<(c_int, libc::shmid_ds), LsIpcError> { + let mut stat: libc::shmid_ds; + let r = unsafe { + stat = std::mem::zeroed(); + libc::shmctl(id, cmd, &mut stat) + }; + + if r == -1 { + Err(LsIpcError::last_io0("shmctl")) + } else { + Ok((r, stat)) + } +} + +struct Limits { + shm_max: u64, + shm_min: u64, + shm_mni: u64, + shm_all: u64, +} + +impl Limits { + fn new() -> Result { + Self::from_proc().or_else(|_| Self::from_syscall()) + } + + fn from_proc() -> Result { + Ok(Self { + shm_max: read_value::(_PATH_PROC_IPC_SHMMAX)?, + shm_min: 1, + shm_mni: read_value::(_PATH_PROC_IPC_SHMMNI)?, + shm_all: read_value::(_PATH_PROC_IPC_SHMALL)?, + }) + } + + fn from_syscall() -> Result { + let (_, stat) = shmctl(0, libc::IPC_INFO)?; + + let shm_info = unsafe { &*(&stat as *const libc::shmid_ds).cast::() }; + + Ok(Self { + shm_max: shm_info.shmmax, + shm_min: shm_info.shmmin, + shm_mni: shm_info.shmmni, + shm_all: shm_info.shmall, + }) + } +} + +struct SysVIpcEntry { + key: libc::key_t, + shmid: c_int, + perms: c_uint, + segsz: u64, + cpid: libc::pid_t, + lpid: libc::pid_t, + nattch: u64, + uid: libc::uid_t, + gid: libc::gid_t, + cuid: libc::uid_t, + cgid: libc::gid_t, + atime: libc::time_t, + dtime: libc::time_t, + ctime: libc::time_t, +} + +impl SysVIpcEntry { + fn from_shmid_ds(shmid: c_int, stat: &libc::shmid_ds) -> Self { + Self { + key: stat.shm_perm.__key, + shmid, + perms: c_uint::from(stat.shm_perm.mode), + segsz: u64::try_from(stat.shm_segsz).unwrap_or(u64::MAX), + cpid: stat.shm_cpid, + lpid: stat.shm_lpid, + nattch: stat.shm_nattch, + uid: stat.shm_perm.uid, + gid: stat.shm_perm.gid, + cuid: stat.shm_perm.cuid, + cgid: stat.shm_perm.cgid, + atime: stat.shm_atime, + dtime: stat.shm_dtime, + ctime: stat.shm_ctime, + } + } + + fn status_desc(&self) -> String { + static STATUS_MAP: [(c_uint, &str); 4] = [ + (SHM_DEST, "dest"), + (SHM_LOCKED, "locked"), + (libc::SHM_HUGETLB as c_uint, "hugetlb"), + (libc::SHM_NORESERVE as c_uint, "noreserve"), + ]; + + let mut separator = ""; + let mut result = String::default(); + + for (flag, name) in STATUS_MAP { + if (self.perms & flag) != 0 { + result.push_str(separator); + result.push_str(name); + separator = ","; + } + } + + result + } +} + +impl FromStr for SysVIpcEntry { + type Err = LsIpcError; + + fn from_str(line: &str) -> Result { + let err_map = || { + let err = io::ErrorKind::InvalidData; + LsIpcError::io1("invalid data", _PATH_PROC_SYSV_SHM, err) + }; + + let mut iter = line.split_ascii_whitespace(); + let key = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let shmid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let perms = iter + .next() + .and_then(|s| c_uint::from_str_radix(s, 8).ok()) + .ok_or_else(err_map)?; + let segsz = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let cpid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let lpid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let nattch = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let uid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let gid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let cuid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let cgid = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let atime = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let dtime = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + let ctime = iter + .next() + .and_then(|s| s.parse().ok()) + .ok_or_else(err_map)?; + + Ok(Self { + key, + shmid, + perms, + segsz, + cpid, + lpid, + nattch, + uid, + gid, + cuid, + cgid, + atime, + dtime, + ctime, + }) + } +} + +struct SysVIpc(Vec); + +impl SysVIpc { + fn new(id: Option) -> Result { + if let Ok(file) = File::open(_PATH_PROC_SYSV_SHM).map(BufReader::new) { + if let Some(id) = id { + Self::from_proc_by_id(id, file) + } else { + Self::from_proc(file) + } + } else if let Some(id) = id { + Self::from_syscall_by_id(id) + } else { + Self::from_syscall() + } + } + + fn from_proc(mut file: BufReader) -> Result { + let err_map = + move |err: std::io::Error| LsIpcError::io1("reading file", _PATH_PROC_SYSV_SHM, err); + + file.skip_until(b'\n').map_err(err_map)?; // Skip the header line. + + let mut list = Vec::default(); + let mut line = String::default(); + + loop { + line.clear(); + if file.read_line(&mut line).map_err(err_map)? == 0 { + break; + } + + let entry = line.parse()?; + list.push(entry); + } + Ok(Self(list)) + } + + fn from_proc_by_id(id: c_uint, mut file: BufReader) -> Result { + let err_map = + move |err: std::io::Error| LsIpcError::io1("reading file", _PATH_PROC_SYSV_SHM, err); + + file.skip_until(b'\n').map_err(err_map)?; // Skip the header line. + + let mut line = String::default(); + + loop { + line.clear(); + if file.read_line(&mut line).map_err(err_map)? == 0 { + break Ok(Self(Vec::default())); + } + + let shmid: c_uint = line + .split_ascii_whitespace() + .nth(1) + .and_then(|s| s.parse().ok()) + .ok_or_else(|| { + let err = io::ErrorKind::InvalidData; + LsIpcError::io1("invalid data", _PATH_PROC_SYSV_SHM, err) + })?; + + if shmid == id { + let entry = line.parse()?; + break Ok(Self(vec![entry])); + } + } + } + + fn from_syscall() -> Result { + let (max_id, _) = shmctl(0, SHM_INFO)?; + + let list = (0..max_id) + .map(|id| shmctl(id, SHM_STAT)) + .filter_map(Result::ok) + .map(|(shmid, stat)| SysVIpcEntry::from_shmid_ds(shmid, &stat)) + .collect(); + + Ok(Self(list)) + } + + fn from_syscall_by_id(id: c_uint) -> Result { + let (max_id, _) = shmctl(0, SHM_INFO)?; + + let list = (0..max_id) + .map(|current_id| shmctl(current_id, SHM_STAT)) + .filter_map(Result::ok) + .filter(|(shmid, _stat)| c_uint::try_from(*shmid).is_ok_and(|shmid| shmid == id)) + .map(|(shmid, stat)| SysVIpcEntry::from_shmid_ds(shmid, &stat)) + .next() + .map_or_else(Vec::default, |entry| vec![entry]); + + Ok(Self(list)) + } +} + +pub(crate) fn print_global( + args: &clap::ArgMatches, + columns: &[&ColumnInfo], + table: &mut Table, +) -> Result<(), LsIpcError> { + let limits = Limits::new()?; + let sys_v_ipc = SysVIpc::new(None)?; + let segsz_pages = sys_v_ipc.0.iter().map(|shm| shm.segsz).sum::() / get_page_size()?; + let in_bytes = args.get_flag(crate::options::BYTES); + + let lines_config = [ + ( + c"SHMMNI", + c"Shared memory segments", + Some(u64::try_from(sys_v_ipc.0.len()).unwrap_or(u64::MAX)), + limits.shm_mni, + true, + ), + ( + c"SHMALL", + c"Shared memory pages", + Some(segsz_pages), + limits.shm_all, + true, + ), + ( + c"SHMMAX", + c"Max size of shared memory segment (bytes)", + None, + limits.shm_max, + in_bytes, + ), + ( + c"SHMMIN", + c"Min size of shared memory segment (bytes)", + None, + limits.shm_min, + in_bytes, + ), + ]; + + lines_config + .into_iter() + .try_for_each(move |(resource, description, used, limit, in_bytes)| { + new_global_line(columns, table, resource, description, used, limit, in_bytes) + }) +} + +pub(crate) fn describe( + args: &clap::ArgMatches, + time_format: crate::TimeFormat, + columns: &[&ColumnInfo], + table: &mut Table, + id: Option, +) -> Result<(), LsIpcError> { + let now = time_of_day()?; + let sys_v_ipc = SysVIpc::new(id)?; + + if let Some(id) = id { + if sys_v_ipc.0.len() != 1 { + eprintln!("id {id} not found"); + return Ok(()); + } + } + + table.set_name(c"sharedmemory")?; + + let mut users = UserDbRecordRef::default(); + let mut groups = GroupDbRecordRef::default(); + + for entry in sys_v_ipc.0 { + let mut line = table.new_line(None)?; + + for (cell_index, &column) in columns.iter().enumerate() { + let data_str = match column.id.to_bytes() { + b"KEY" => describe_key(entry.key), + b"ID" => describe_integer(entry.shmid), + b"CUID" => describe_integer(entry.cuid), + b"CGID" => describe_integer(entry.cgid), + b"UID" => describe_integer(entry.uid), + b"GID" => describe_integer(entry.gid), + b"NATTCH" => describe_integer(entry.nattch), + b"CPID" => describe_integer(entry.cpid), + b"LPID" => describe_integer(entry.lpid), + b"CTIME" => format_time(time_format, &now, entry.ctime)?.map(Cow::Owned), + b"ATTACH" => format_time(time_format, &now, entry.atime)?.map(Cow::Owned), + b"DETACH" => format_time(time_format, &now, entry.dtime)?.map(Cow::Owned), + b"COMMAND" => pid_command_line(entry.cpid).map(Cow::Owned).map(Some)?, + b"STATUS" => Some(Cow::Owned(CString::new(entry.status_desc()).unwrap())), + b"SIZE" => describe_size(entry.segsz, args.get_flag(crate::options::BYTES)), + b"OWNER" => describe_owner(&mut users, entry.uid), + b"CUSER" => users.for_id(entry.cuid).name().map(Cow::Borrowed), + b"USER" => users.for_id(entry.uid).name().map(Cow::Borrowed), + b"CGROUP" => groups.for_id(entry.cgid).name().map(Cow::Borrowed), + b"GROUP" => groups.for_id(entry.gid).name().map(Cow::Borrowed), + b"PERMS" => describe_permissions(args, entry.perms & 0o777), + + _ => continue, + }; + + if let Some(data_str) = data_str { + line.set_data(cell_index, &data_str)?; + } + } + } + Ok(()) +} diff --git a/src/uu/lsipc/src/smartcols.rs b/src/uu/lsipc/src/smartcols.rs new file mode 100644 index 0000000..92b325b --- /dev/null +++ b/src/uu/lsipc/src/smartcols.rs @@ -0,0 +1,225 @@ +// 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. + +use std::ffi::{CStr, c_int, c_uint, c_void}; +use std::ptr::NonNull; +use std::{io, mem, ptr}; + +use smartcols_sys::{ + SCOLS_ITER_BACKWARD, SCOLS_ITER_FORWARD, libscols_cell, libscols_column, libscols_iter, + libscols_line, libscols_table, scols_cell_get_data, scols_free_iter, scols_init_debug, + scols_line_get_cell, scols_line_get_userdata, scols_line_set_data, scols_line_set_userdata, + scols_new_iter, scols_new_table, scols_print_table, scols_table_enable_export, + scols_table_enable_json, scols_table_enable_noheadings, scols_table_enable_raw, + scols_table_enable_shellvar, scols_table_get_line, scols_table_new_column, + scols_table_new_line, scols_table_next_column, scols_table_set_column_separator, + scols_table_set_name, scols_unref_table, +}; + +use crate::errors::LsIpcError; + +pub(crate) fn initialize() { + unsafe { scols_init_debug(0) }; +} + +#[repr(transparent)] +pub(crate) struct TableRef(NonNull); + +impl From> for TableRef { + fn from(value: NonNull) -> Self { + Self(value) + } +} + +impl TableOperations for TableRef { + fn as_ptr(&self) -> *mut libscols_table { + self.0.as_ptr() + } +} + +#[repr(transparent)] +pub(crate) struct Table(NonNull); + +impl Table { + pub(crate) fn new() -> Result { + NonNull::new(unsafe { scols_new_table() }) + .ok_or_else(|| LsIpcError::io0("scols_new_table", io::ErrorKind::OutOfMemory)) + .map(Self) + } + + pub(crate) fn into_inner(self) -> NonNull { + let ptr = self.0; + mem::forget(self); + ptr + } +} + +impl TableOperations for Table { + fn as_ptr(&self) -> *mut libscols_table { + self.0.as_ptr() + } +} + +impl Drop for Table { + fn drop(&mut self) { + unsafe { scols_unref_table(self.0.as_ptr()) } + } +} + +pub(crate) trait TableOperations: Sized { + fn as_ptr(&self) -> *mut libscols_table; + + fn enable_headings(&mut self, enable: bool) -> Result<(), LsIpcError> { + let no_headings = c_int::from(!enable); + let r = unsafe { scols_table_enable_noheadings(self.as_ptr(), no_headings) }; + LsIpcError::io_from_neg_errno("scols_table_enable_noheadings", r).map(|_| ()) + } + + fn enable_shell_variable(&mut self, enable: bool) -> Result<(), LsIpcError> { + let r = unsafe { scols_table_enable_shellvar(self.as_ptr(), c_int::from(enable)) }; + LsIpcError::io_from_neg_errno("scols_table_enable_shellvar", r).map(|_| ()) + } + + fn enable_export(&mut self, enable: bool) -> Result<(), LsIpcError> { + let r = unsafe { scols_table_enable_export(self.as_ptr(), c_int::from(enable)) }; + LsIpcError::io_from_neg_errno("scols_table_enable_export", r).map(|_| ()) + } + + fn enable_raw(&mut self, enable: bool) -> Result<(), LsIpcError> { + let r = unsafe { scols_table_enable_raw(self.as_ptr(), c_int::from(enable)) }; + LsIpcError::io_from_neg_errno("scols_table_enable_raw", r).map(|_| ()) + } + + fn enable_json(&mut self, enable: bool) -> Result<(), LsIpcError> { + let r = unsafe { scols_table_enable_json(self.as_ptr(), c_int::from(enable)) }; + LsIpcError::io_from_neg_errno("scols_table_enable_json", r).map(|_| ()) + } + + fn set_column_separator(&mut self, separator: &CStr) -> Result<(), LsIpcError> { + let r = unsafe { scols_table_set_column_separator(self.as_ptr(), separator.as_ptr()) }; + LsIpcError::io_from_neg_errno("scols_table_set_column_separator", r).map(|_| ()) + } + + fn new_column( + &mut self, + name: &CStr, + width_hint: f64, + flags: c_uint, + ) -> Result { + NonNull::new(unsafe { + scols_table_new_column(self.as_ptr(), name.as_ptr(), width_hint, flags as c_int) + }) + .ok_or_else(|| LsIpcError::io0("scols_table_new_column", io::ErrorKind::OutOfMemory)) + .map(ColumnRef) + } + + fn new_line(&mut self, parent: Option<&mut LineRef>) -> Result { + let parent = parent.map_or(ptr::null_mut(), |parent| parent.0.as_ptr()); + + NonNull::new(unsafe { scols_table_new_line(self.as_ptr(), parent) }) + .ok_or_else(|| LsIpcError::io0("scols_table_new_line", io::ErrorKind::OutOfMemory)) + .map(LineRef) + } + + fn set_name(&mut self, name: &CStr) -> Result<(), LsIpcError> { + let r = unsafe { scols_table_set_name(self.as_ptr(), name.as_ptr()) }; + LsIpcError::io_from_neg_errno("scols_table_set_name", r).map(|_| ()) + } + + fn line(&self, column_index: usize) -> Result { + NonNull::new(unsafe { scols_table_get_line(self.as_ptr(), column_index) }) + .ok_or_else(|| LsIpcError::io0("scols_table_get_line", io::ErrorKind::InvalidInput)) + .map(LineRef) + } + + fn column_iter(&self, direction: IterDirection) -> Result, LsIpcError> { + let iter = NonNull::new(unsafe { scols_new_iter(direction as c_int) }) + .ok_or_else(|| LsIpcError::io0("scols_new_iter", io::ErrorKind::OutOfMemory))?; + Ok(ColumnIter { table: self, iter }) + } + + fn print(&self) -> Result<(), LsIpcError> { + let r = unsafe { scols_print_table(self.as_ptr()) }; + LsIpcError::io_from_neg_errno("scols_print_table", r).map(|_| ()) + } +} + +#[repr(transparent)] +pub(crate) struct LineRef(NonNull); + +impl LineRef { + pub(crate) fn user_data(&self) -> *mut c_void { + unsafe { scols_line_get_userdata(self.0.as_ptr()) } + } + + pub(crate) fn set_user_data(&mut self, user_data: *mut c_void) -> Result<(), LsIpcError> { + let r = unsafe { scols_line_set_userdata(self.0.as_ptr(), user_data) }; + LsIpcError::io_from_neg_errno("scols_line_set_userdata", r).map(|_| ()) + } + + pub(crate) fn set_data(&mut self, cell_index: usize, data: &CStr) -> Result<(), LsIpcError> { + let r = unsafe { scols_line_set_data(self.0.as_ptr(), cell_index, data.as_ptr()) }; + LsIpcError::io_from_neg_errno("scols_line_set_data", r).map(|_| ()) + } + + pub(crate) fn cell(&self, cell_index: usize) -> Result { + NonNull::new(unsafe { scols_line_get_cell(self.0.as_ptr(), cell_index) }) + .ok_or_else(|| LsIpcError::io0("scols_line_get_cell", io::ErrorKind::InvalidInput)) + .map(CellRef) + } +} + +#[repr(transparent)] +pub(crate) struct ColumnRef(NonNull); + +#[repr(transparent)] +pub(crate) struct CellRef(NonNull); + +impl CellRef { + pub(crate) fn data_as_c_str(&self) -> Option<&CStr> { + unsafe { + let data_ptr = scols_cell_get_data(self.0.as_ptr()); + (!data_ptr.is_null()).then(|| CStr::from_ptr(data_ptr)) + } + } +} + +#[allow(dead_code)] +#[derive(Debug, Clone, Copy)] +#[repr(i32)] +pub(crate) enum IterDirection { + Forward = SCOLS_ITER_FORWARD as i32, + Backward = SCOLS_ITER_BACKWARD as i32, +} + +pub(crate) struct ColumnIter<'table, T: TableOperations> { + table: &'table T, + iter: NonNull, +} + +impl Iterator for ColumnIter<'_, T> { + type Item = Result; + + fn next(&mut self) -> Option { + let mut column = ptr::null_mut(); + let table_ptr = self.table.as_ptr(); + let r = unsafe { scols_table_next_column(table_ptr, self.iter.as_ptr(), &mut column) }; + + match LsIpcError::io_from_neg_errno("scols_table_next_column", r) { + Err(err) => Some(Err(err)), + + Ok(r) => NonNull::new(column) + .filter(|_| r == 0) + .map(ColumnRef) + .map(Ok), + } + } +} + +impl Drop for ColumnIter<'_, T> { + fn drop(&mut self) { + unsafe { scols_free_iter(self.iter.as_ptr()) } + } +} diff --git a/src/uu/lsipc/src/utils.rs b/src/uu/lsipc/src/utils.rs new file mode 100644 index 0000000..42c8243 --- /dev/null +++ b/src/uu/lsipc/src/utils.rs @@ -0,0 +1,165 @@ +// 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. + +use std::ffi::{CStr, CString}; +use std::fs::File; +use std::io::{BufRead, BufReader}; +use std::path::Path; +use std::str::FromStr; +use std::sync::atomic::AtomicU64; +use std::{io, ptr}; + +use crate::errors::LsIpcError; + +pub(crate) fn get_page_size() -> Result { + use std::sync::atomic::Ordering; + + static VALUE: AtomicU64 = AtomicU64::new(0); + + let mut page_size = VALUE.load(Ordering::Acquire); + + if page_size == 0 { + let value = unsafe { libc::sysconf(libc::_SC_PAGE_SIZE) }; + if value == -1 || value == 0 { + return Err(LsIpcError::last_io0("sysconf(_SC_PAGE_SIZE)")); + } + + let value = value as u64; + + match VALUE.compare_exchange(0, value, Ordering::AcqRel, Ordering::Acquire) { + Ok(_previous_value) => page_size = value, + Err(previous_value) => page_size = previous_value, + } + } + + Ok(page_size) +} + +pub(crate) fn local_time(time: libc::time_t) -> Result { + let mut detailed_time: libc::tm; + + let r = unsafe { + detailed_time = std::mem::zeroed(); + libc::localtime_r(&time, &mut detailed_time) + }; + + if r.is_null() { + Err(LsIpcError::last_io0("localtime_r")) + } else { + Ok(detailed_time) + } +} + +pub(crate) fn time_of_day() -> Result { + let mut time: libc::timeval; + let r = unsafe { + time = std::mem::zeroed(); + libc::gettimeofday(&mut time, ptr::null_mut()) + }; + + if r == -1 { + Err(LsIpcError::last_io0("gettimeofday")) + } else { + Ok(time) + } +} + +pub(crate) fn pid_command_line(pid: libc::pid_t) -> Result { + let path = Path::new("/proc").join(pid.to_string()).join("cmdline"); + + let mut contents = + std::fs::read(&path).map_err(|err| LsIpcError::io1("reading file", path, err))?; + if contents.last().copied() == Some(0_u8) { + contents.pop(); + } + find_replace_in_vec(0_u8, b' ', &mut contents); + contents.push(0_u8); + + Ok(unsafe { CString::from_vec_with_nul_unchecked(contents) }) +} + +pub(crate) fn find_replace_in_vec(needle: T, replacement: T, data: &mut [T]) +where + T: Copy + Eq, +{ + let mut start = 0; + while let Some((index, found)) = data[start..] + .iter_mut() + .enumerate() + .find(|&(_index, &mut element)| element == needle) + { + *found = replacement; + + start += index + 1; + if start >= data.len() { + break; + } + } +} + +pub(crate) fn read_value(path: impl AsRef) -> Result +where + T: FromStr, +{ + let path = path.as_ref(); + + let mut file = File::open(path) + .map(BufReader::new) + .map_err(|err| LsIpcError::io1("opening file", path, err))?; + + let mut line = String::default(); + file.read_line(&mut line) + .map_err(|err| LsIpcError::io1("reading file", path, err))?; + + line.trim() + .parse() + .map_err(|_| LsIpcError::io1("invalid data", path, io::ErrorKind::InvalidData)) +} + +pub(crate) struct UserDbRecordRef(*const libc::passwd); + +impl Default for UserDbRecordRef { + fn default() -> Self { + Self(ptr::null()) + } +} + +impl UserDbRecordRef { + pub(crate) fn for_id(&mut self, uid: libc::uid_t) -> &Self { + if unsafe { self.0.as_ref() }.is_none_or(|record| record.pw_uid != uid) { + self.0 = unsafe { libc::getpwuid(uid) }; + } + self + } + + pub(crate) fn name(&self) -> Option<&CStr> { + unsafe { self.0.as_ref() } + .and_then(|record| (!record.pw_name.is_null()).then_some(record.pw_name)) + .map(|name| unsafe { CStr::from_ptr(name) }) + } +} + +pub(crate) struct GroupDbRecordRef(*const libc::group); + +impl Default for GroupDbRecordRef { + fn default() -> Self { + Self(ptr::null()) + } +} + +impl GroupDbRecordRef { + pub(crate) fn for_id(&mut self, gid: libc::gid_t) -> &Self { + if unsafe { self.0.as_ref() }.is_none_or(|record| record.gr_gid != gid) { + self.0 = unsafe { libc::getgrgid(gid) }; + } + self + } + + pub(crate) fn name(&self) -> Option<&CStr> { + unsafe { self.0.as_ref() } + .and_then(|record| (!record.gr_name.is_null()).then_some(record.gr_name)) + .map(|name| unsafe { CStr::from_ptr(name) }) + } +}