diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c2c0f69..18b98dd2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,10 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - if: ${{ contains(matrix.os, 'ubuntu') }} + run: | + sudo apt-get update -y + sudo apt-get -yq --no-install-suggests --no-install-recommends install libsystemd-dev - run: cargo check test: @@ -26,6 +30,10 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - if: ${{ contains(matrix.os, 'ubuntu') }} + run: | + sudo apt-get update -y + sudo apt-get -yq --no-install-suggests --no-install-recommends install libsystemd-dev - run: cargo test --all fmt: @@ -71,6 +79,10 @@ jobs: uses: dtolnay/rust-toolchain@nightly with: components: llvm-tools-preview + - if: ${{ contains(matrix.job.os, 'ubuntu') }} + run: | + sudo apt-get update -y + sudo apt-get -yq --no-install-suggests --no-install-recommends install libsystemd-dev - name: Test run: cargo test --no-fail-fast env: diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 04837e29..d1775f12 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -81,6 +81,10 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Run sccache-cache uses: mozilla-actions/sccache-action@v0.0.8 + - if: ${{ contains(matrix.job.os, 'ubuntu') }} + run: | + sudo apt-get update -y + sudo apt-get -yq --no-install-suggests --no-install-recommends install libsystemd-dev - name: Initialize workflow variables id: vars shell: bash diff --git a/Cargo.lock b/Cargo.lock index 52320da1..0c3ba95a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,6 +92,12 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytesize" version = "2.0.1" @@ -573,6 +579,12 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -680,7 +692,7 @@ checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94" dependencies = [ "rand_chacha", "rand_core 0.9.0", - "zerocopy", + "zerocopy 0.8.14", ] [[package]] @@ -706,7 +718,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" dependencies = [ "getrandom 0.3.1", - "zerocopy", + "zerocopy 0.8.14", ] [[package]] @@ -1054,6 +1066,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "utmp-classic" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24c654e19afaa6b8f3877ece5d3bed849c2719c56f6752b18ca7da4fcc6e85a" +dependencies = [ + "cfg-if", + "libc", + "thiserror 1.0.69", + "time", + "utmp-classic-raw", + "zerocopy 0.7.35", +] + +[[package]] +name = "utmp-classic-raw" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22c226537a3d6e01c440c1926ca0256dbee2d19b2229ede6fc4863a6493dd831" +dependencies = [ + "cfg-if", + "zerocopy 0.7.35", +] + [[package]] name = "uu_free" version = "0.0.1" @@ -1173,13 +1209,16 @@ dependencies = [ name = "uu_top" version = "0.0.1" dependencies = [ + "bytesize", "chrono", "clap", "libc", "nix", + "pkg-config", "prettytable-rs", "sysinfo", "uucore", + "windows-sys 0.59.0", ] [[package]] @@ -1216,9 +1255,12 @@ dependencies = [ "nix", "number_prefix", "os_display", + "thiserror 2.0.12", "time", + "utmp-classic", "uucore_procs", "wild", + "windows-sys 0.59.0", ] [[package]] @@ -1693,13 +1735,34 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + [[package]] name = "zerocopy" version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.8.14", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 02be893b..37cbb449 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,6 +62,7 @@ thiserror = "2.0.4" uucore = "0.0.30" walkdir = "2.5.0" windows = { version = "0.60.0" } +windows-sys = { version = "0.59.0", default-features = false } xattr = "1.3.1" [dependencies] diff --git a/src/uu/top/Cargo.toml b/src/uu/top/Cargo.toml index 800a2fc3..f02dd87f 100644 --- a/src/uu/top/Cargo.toml +++ b/src/uu/top/Cargo.toml @@ -12,13 +12,22 @@ keywords = ["acl", "uutils", "cross-platform", "cli", "utility"] categories = ["command-line-utilities"] [dependencies] -uucore = { workspace = true, features = ["utmpx"] } +uucore = { workspace = true, features = ["utmpx", "uptime"] } clap = { workspace = true } libc = { workspace = true } nix = { workspace = true } prettytable-rs = { workspace = true } sysinfo = { workspace = true } chrono = { workspace = true } +bytesize = { workspace = true } +[target.'cfg(target_os="windows")'.dependencies] +windows-sys = { workspace = true, features = [ + "Win32_System_RemoteDesktop", + "Win32_System_SystemInformation", +] } + +[target.'cfg(target_os="linux")'.build-dependencies] +pkg-config = "0.3.31" [lib] path = "src/top.rs" diff --git a/src/uu/top/build.rs b/src/uu/top/build.rs new file mode 100644 index 00000000..8adf5b0a --- /dev/null +++ b/src/uu/top/build.rs @@ -0,0 +1,4 @@ +fn main() { + #[cfg(target_os = "linux")] + pkg_config::find_library("libsystemd").unwrap(); +} diff --git a/src/uu/top/src/header.rs b/src/uu/top/src/header.rs new file mode 100644 index 00000000..1785e200 --- /dev/null +++ b/src/uu/top/src/header.rs @@ -0,0 +1,282 @@ +// This file is part of the uutils procps package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::picker::sysinfo; +use bytesize::ByteSize; +use uucore::uptime::{ + get_formated_uptime, get_formatted_loadavg, get_formatted_nusers, get_formatted_time, +}; + +pub(crate) fn header(scale_summary_mem: Option<&String>) -> String { + format!( + "top - {time} {uptime}, {user}, {load_average}\n\ + {task}\n\ + {cpu}\n\ + {memory}", + time = get_formatted_time(), + uptime = uptime(), + user = user(), + load_average = load_average(), + task = task(), + cpu = cpu(), + memory = memory(scale_summary_mem), + ) +} + +#[cfg(target_os = "linux")] +extern "C" { + pub fn sd_booted() -> libc::c_int; + pub fn sd_get_sessions(sessions: *mut *mut *mut libc::c_char) -> libc::c_int; + pub fn sd_session_get_class( + session: *const libc::c_char, + class: *mut *mut libc::c_char, + ) -> libc::c_int; +} + +fn format_memory(memory_b: u64, unit: u64) -> f64 { + ByteSize::b(memory_b).0 as f64 / unit as f64 +} + +fn uptime() -> String { + get_formated_uptime(None).unwrap_or_default() +} + +#[cfg(target_os = "linux")] +pub fn get_nusers_systemd() -> uucore::error::UResult { + use std::ffi::CStr; + use std::ptr; + use uucore::error::USimpleError; + use uucore::libc::*; + + // SAFETY: sd_booted to check if system is booted with systemd. + unsafe { + // systemd + if sd_booted() > 0 { + let mut sessions_list: *mut *mut c_char = ptr::null_mut(); + let mut num_user = 0; + let sessions = sd_get_sessions(&mut sessions_list); + + if sessions > 0 { + for i in 0..sessions { + let mut class: *mut c_char = ptr::null_mut(); + + if sd_session_get_class( + *sessions_list.add(i as usize) as *const c_char, + &mut class, + ) < 0 + { + continue; + } + if CStr::from_ptr(class).to_str().unwrap().starts_with("user") { + num_user += 1; + } + free(class as *mut c_void); + } + } + + for i in 0..sessions { + free(*sessions_list.add(i as usize) as *mut c_void); + } + free(sessions_list as *mut c_void); + + return Ok(num_user); + } + } + Err(USimpleError::new( + 1, + "could not retrieve number of logged users", + )) +} + +// see: https://gitlab.com/procps-ng/procps/-/blob/4740a0efa79cade867cfc7b32955fe0f75bf5173/library/uptime.c#L63-L115 +fn user() -> String { + #[cfg(target_os = "linux")] + if let Ok(nusers) = get_nusers_systemd() { + return uucore::uptime::format_nusers(nusers); + } + + get_formatted_nusers() +} + +fn load_average() -> String { + get_formatted_loadavg().unwrap_or_default() +} + +fn task() -> String { + let binding = sysinfo().read().unwrap(); + + let process = binding.processes(); + let mut running_process = 0; + let mut sleeping_process = 0; + let mut stopped_process = 0; + let mut zombie_process = 0; + + for (_, process) in process.iter() { + match process.status() { + sysinfo::ProcessStatus::Run => running_process += 1, + sysinfo::ProcessStatus::Sleep => sleeping_process += 1, + sysinfo::ProcessStatus::Stop => stopped_process += 1, + sysinfo::ProcessStatus::Zombie => zombie_process += 1, + _ => {} + }; + } + + format!( + "Tasks: {} total, {} running, {} sleeping, {} stopped, {} zombie", + process.len(), + running_process, + sleeping_process, + stopped_process, + zombie_process, + ) +} + +#[cfg(target_os = "linux")] +fn cpu() -> String { + let file = std::fs::File::open(std::path::Path::new("/proc/stat")).unwrap(); + let content = std::io::read_to_string(file).unwrap(); + let load = content + .lines() + .next() + .unwrap() + .strip_prefix("cpu") + .unwrap() + .split(' ') + .filter(|s| !s.is_empty()) + .collect::>(); + let user = load[0].parse::().unwrap(); + let nice = load[1].parse::().unwrap(); + let system = load[2].parse::().unwrap(); + let idle = load[3].parse::().unwrap_or_default(); // since 2.5.41 + let io_wait = load[4].parse::().unwrap_or_default(); // since 2.5.41 + let hardware_interrupt = load[5].parse::().unwrap_or_default(); // since 2.6.0 + let software_interrupt = load[6].parse::().unwrap_or_default(); // since 2.6.0 + let steal_time = load[7].parse::().unwrap_or_default(); // since 2.6.11 + // GNU do not show guest and guest_nice + let guest = load[8].parse::().unwrap_or_default(); // since 2.6.24 + let guest_nice = load[9].parse::().unwrap_or_default(); // since 2.6.33 + let total = user + + nice + + system + + idle + + io_wait + + hardware_interrupt + + software_interrupt + + steal_time + + guest + + guest_nice; + + format!( + "%Cpu(s): {:.1} us, {:.1} sy, {:.1} ni, {:.1} id, {:.1} wa, {:.1} hi, {:.1} si, {:.1} st", + user / total * 100.0, + system / total * 100.0, + nice / total * 100.0, + idle / total * 100.0, + io_wait / total * 100.0, + hardware_interrupt / total * 100.0, + software_interrupt / total * 100.0, + steal_time / total * 100.0, + ) +} + +#[cfg(target_os = "windows")] +fn cpu() -> String { + use libc::malloc; + use windows_sys::Wdk::System::SystemInformation::NtQuerySystemInformation; + + #[repr(C)] + #[derive(Debug)] + struct SystemProcessorPerformanceInformation { + idle_time: i64, // LARGE_INTEGER + kernel_time: i64, // LARGE_INTEGER + user_time: i64, // LARGE_INTEGER + dpc_time: i64, // LARGE_INTEGER + interrupt_time: i64, // LARGE_INTEGER + interrupt_count: u32, // ULONG + } + + let n_cpu = sysinfo().read().unwrap().cpus().len(); + let mut cpu_load = SystemProcessorPerformanceInformation { + idle_time: 0, + kernel_time: 0, + user_time: 0, + dpc_time: 0, + interrupt_time: 0, + interrupt_count: 0, + }; + // SAFETY: malloc is safe to use here. We free the memory after we are done with it. If action fails, all "time" will be 0. + unsafe { + let len = n_cpu * size_of::(); + let data = malloc(len); + let status = NtQuerySystemInformation( + windows_sys::Wdk::System::SystemInformation::SystemProcessorPerformanceInformation, + data, + (n_cpu * size_of::()) as u32, + std::ptr::null_mut(), + ); + if status == 0 { + for i in 0..n_cpu { + let cpu = data.add(i * size_of::()) + as *const SystemProcessorPerformanceInformation; + let cpu = cpu.as_ref().unwrap(); + cpu_load.idle_time += cpu.idle_time; + cpu_load.kernel_time += cpu.kernel_time; + cpu_load.user_time += cpu.user_time; + cpu_load.dpc_time += cpu.dpc_time; + cpu_load.interrupt_time += cpu.interrupt_time; + cpu_load.interrupt_count += cpu.interrupt_count; + } + } + } + let total = cpu_load.idle_time + + cpu_load.kernel_time + + cpu_load.user_time + + cpu_load.dpc_time + + cpu_load.interrupt_time; + format!( + "%Cpu(s): {:.1} us, {:.1} sy, {:.1} id, {:.1} hi, {:.1} si", + cpu_load.user_time as f64 / total as f64 * 100.0, + cpu_load.kernel_time as f64 / total as f64 * 100.0, + cpu_load.idle_time as f64 / total as f64 * 100.0, + cpu_load.interrupt_time as f64 / total as f64 * 100.0, + cpu_load.dpc_time as f64 / total as f64 * 100.0, + ) +} + +//TODO: Implement for macos +#[cfg(target_os = "macos")] +fn cpu() -> String { + "TODO".into() +} + +fn memory(scale_summary_mem: Option<&String>) -> String { + let binding = sysinfo().read().unwrap(); + let (unit, unit_name) = match scale_summary_mem { + Some(scale) => match scale.as_str() { + "k" => (bytesize::KIB, "KiB"), + "m" => (bytesize::MIB, "MiB"), + "g" => (bytesize::GIB, "GiB"), + "t" => (bytesize::TIB, "TiB"), + "p" => (bytesize::PIB, "PiB"), + "e" => (1_152_921_504_606_846_976, "EiB"), + _ => (bytesize::MIB, "MiB"), + }, + None => (bytesize::MIB, "MiB"), + }; + + format!( + "{unit_name} Mem : {:8.1} total, {:8.1} free, {:8.1} used, {:8.1} buff/cache\n\ + {unit_name} Swap: {:8.1} total, {:8.1} free, {:8.1} used, {:8.1} avail Mem", + format_memory(binding.total_memory(), unit), + format_memory(binding.free_memory(), unit), + format_memory(binding.used_memory(), unit), + format_memory(binding.available_memory() - binding.free_memory(), unit), + format_memory(binding.total_swap(), unit), + format_memory(binding.free_swap(), unit), + format_memory(binding.used_swap(), unit), + format_memory(binding.available_memory(), unit), + unit_name = unit_name + ) +} diff --git a/src/uu/top/src/top.rs b/src/uu/top/src/top.rs index 6c4a12cc..885ebd19 100644 --- a/src/uu/top/src/top.rs +++ b/src/uu/top/src/top.rs @@ -4,6 +4,7 @@ // file that was distributed with this source code. use clap::{arg, crate_version, value_parser, ArgAction, ArgGroup, ArgMatches, Command}; +use header::header; use picker::pickers; use picker::sysinfo; use prettytable::{format::consts::FORMAT_CLEAN, Row, Table}; @@ -18,6 +19,7 @@ const ABOUT: &str = help_about!("top.md"); const USAGE: &str = help_usage!("top.md"); mod field; +mod header; mod picker; #[allow(unused)] @@ -97,7 +99,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { table }; - println!("{}", header()); + println!("{}", header(matches.get_one::("scale-summary-mem"))); println!("\n"); let cutter = { @@ -157,11 +159,6 @@ where } } -// TODO: Implement information collecting. -fn header() -> String { - "TODO".into() -} - // TODO: Implement fields selecting fn selected_fields() -> Vec { vec![ @@ -261,7 +258,7 @@ pub fn uu_app() -> Command { // arg!(-b --"batch-mode" "run in non-interactive batch mode"), // arg!(-c --"cmdline-toggle" "reverse last remembered 'c' state"), // arg!(-d --delay "iterative delay as SECS [.TENTHS]"), - // arg!(-E --"scale-summary-mem" "set mem as: k,m,g,t,p,e for SCALE"), + arg!(-E --"scale-summary-mem" "set mem as: k,m,g,t,p,e for SCALE"), // arg!(-e --"scale-task-mem" "set mem with: k,m,g,t,p for SCALE"), // arg!(-H --"threads-show" "show tasks plus all their threads"), // arg!(-i --"idle-toggle" "reverse last remembered 'i' state"),