diff --git a/Cargo.toml b/Cargo.toml index 57ec326746e..88c902bf19a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -292,7 +292,7 @@ feat_os_unix_musl = [ "feat_require_unix_utmpx", ] # "feat_os_windows" == set of utilities which can be built/run on modern windows platforms -feat_os_windows = ["feat_Tier1", "kill", "stdbuf", "timeout"] +feat_os_windows = ["feat_Tier1", "kill", "stdbuf", "timeout", "uptime"] ## (secondary platforms) feature sets # "feat_os_unix_gnueabihf" == set of utilities which can be built/run on the "arm-unknown-linux-gnueabihf" target (ARMv6 Linux [hardfloat]) feat_os_unix_gnueabihf = [ diff --git a/src/uu/uptime/src/platform/mod.rs b/src/uu/uptime/src/platform/mod.rs new file mode 100644 index 00000000000..0425bc19616 --- /dev/null +++ b/src/uu/uptime/src/platform/mod.rs @@ -0,0 +1,21 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Platform-specific pieces of `uptime`: the utmp-file operand (which only +//! exists where utmp files do) and the boot-time source for `--since`. The +//! shared control flow in `uptime.rs` only talks to the facade functions +//! re-exported here, which both submodules provide with identical +//! signatures; on Windows the utmp-file facades degenerate to +//! identity/`None`. + +#[cfg(unix)] +mod unix; +#[cfg(unix)] +pub(crate) use unix::*; + +#[cfg(windows)] +mod windows; +#[cfg(windows)] +pub(crate) use windows::*; diff --git a/src/uu/uptime/src/platform/unix.rs b/src/uu/uptime/src/platform/unix.rs new file mode 100644 index 00000000000..65e76b3d7df --- /dev/null +++ b/src/uu/uptime/src/platform/unix.rs @@ -0,0 +1,175 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore loadavg nusers upsecs utmpxname couldnt + +//! Unix implementation of `uptime`'s platform facade: the utmp-file operand +//! and the utmpx-derived boot time for `--since` (on OpenBSD, which has no +//! utmpx binding, boot time comes from [`get_uptime`] directly). + +use clap::{Arg, ArgAction, ArgMatches, Command, ValueHint, builder::ValueParser}; +use std::ffi::OsString; +use std::io::{Write, stdout}; +use uucore::error::UResult; +#[cfg(not(target_os = "openbsd"))] +use uucore::libc::time_t; +use uucore::translate; +use uucore::uptime::get_uptime; +#[cfg(not(target_os = "openbsd"))] +use uucore::utmpx::{BOOT_TIME, USER_PROCESS, Utmpx}; + +use crate::{UptimeError, options, print_loadavg, print_nusers, print_time, print_uptime}; + +/// Register platform-only CLI arguments: the utmp file operand, which only +/// makes sense where utmp files exist. +pub(crate) fn add_platform_args(cmd: Command) -> Command { + cmd.arg( + Arg::new(options::PATH) + .help(translate!("uptime-help-path")) + .action(ArgAction::Set) + .num_args(0..=1) + .value_parser(ValueParser::os_string()) + .value_hint(ValueHint::AnyPath), + ) +} + +/// Run `uptime` against a user-supplied utmp file if the file operand was +/// given, or return `None` to fall through to the default system sources. +pub(crate) fn maybe_uptime_from_file(matches: &ArgMatches) -> Option> { + matches + .get_one::(options::PATH) + .map(uptime_with_file) +} + +/// The system uptime in seconds, for `--since`: derived from the utmpx +/// `BOOT_TIME` record where utmpx is available (on OpenBSD, from +/// [`get_uptime`] directly). +pub(crate) fn system_uptime_seconds() -> UResult { + #[cfg(not(target_os = "openbsd"))] + { + let (boot_time, _) = process_utmpx(None); + get_uptime(boot_time) + } + #[cfg(target_os = "openbsd")] + get_uptime(None) +} + +fn uptime_with_file(file_path: &OsString) -> UResult<()> { + use std::fs; + use std::os::unix::fs::FileTypeExt; + use uucore::error::set_exit_code; + use uucore::show_error; + + // Uptime will print loadavg and time to stderr unless we encounter an extra operand. + let mut non_fatal_error = false; + + // process_utmpx_from_file() doesn't detect or report failures, we check if the path is valid + // before proceeding with more operations. + let md_res = fs::metadata(file_path); + if let Ok(md) = md_res { + if md.is_dir() { + show_error!("{}", UptimeError::TargetIsDir); + non_fatal_error = true; + set_exit_code(1); + } + if md.file_type().is_fifo() { + show_error!("{}", UptimeError::TargetIsFifo); + non_fatal_error = true; + set_exit_code(1); + } + } else if let Err(e) = md_res { + non_fatal_error = true; + set_exit_code(1); + show_error!("{}", UptimeError::IoErr(e)); + } + // utmpxname() returns an -1 , when filename doesn't end with 'x' or its too long. + // Reference: `` + + #[cfg(target_os = "macos")] + { + use std::os::unix::ffi::OsStrExt; + let bytes = file_path.as_os_str().as_bytes(); + + if bytes[bytes.len() - 1] != b'x' { + show_error!("{}", translate!("uptime-error-couldnt-get-boot-time")); + print_time()?; + write!(stdout(), "{}", translate!("uptime-output-unknown-uptime"))?; + print_nusers(Some(0))?; + print_loadavg()?; + set_exit_code(1); + return Ok(()); + } + } + + if non_fatal_error { + print_time()?; + write!(stdout(), "{}", translate!("uptime-output-unknown-uptime"))?; + print_nusers(Some(0))?; + print_loadavg()?; + return Ok(()); + } + + print_time()?; + let user_count; + + #[cfg(not(target_os = "openbsd"))] + { + let (boot_time, count) = process_utmpx(Some(file_path)); + if let Some(time) = boot_time { + print_uptime(Some(time))?; + } else { + show_error!("{}", translate!("uptime-error-couldnt-get-boot-time")); + set_exit_code(1); + + write!(stdout(), "{}", translate!("uptime-output-unknown-uptime"))?; + } + user_count = count; + } + + #[cfg(target_os = "openbsd")] + { + let upsecs = get_uptime(None)?; + if upsecs >= 0 { + print_uptime(Some(upsecs))?; + } else { + show_error!("{}", translate!("uptime-error-couldnt-get-boot-time")); + set_exit_code(1); + + write!(stdout(), "{}", translate!("uptime-output-unknown-uptime"))?; + } + user_count = + uucore::uptime::get_nusers(file_path.to_str().expect("invalid utmp path file")); + } + + print_nusers(Some(user_count))?; + print_loadavg()?; + + Ok(()) +} + +#[cfg(not(target_os = "openbsd"))] +fn process_utmpx(file: Option<&OsString>) -> (Option, usize) { + let mut nusers = 0; + let mut boot_time = None; + + let records = match file { + Some(f) => Utmpx::iter_all_records_from(f), + None => Utmpx::iter_all_records(), + }; + + for line in records { + match line.record_type() { + x if x == USER_PROCESS => nusers += 1, + x if x == BOOT_TIME => { + let dt = line.login_time(); + if dt.unix_timestamp() > 0 { + boot_time = Some(dt.unix_timestamp() as time_t); + } + } + _ => (), + } + } + (boot_time, nusers) +} diff --git a/src/uu/uptime/src/platform/windows.rs b/src/uu/uptime/src/platform/windows.rs new file mode 100644 index 00000000000..0b63b374929 --- /dev/null +++ b/src/uu/uptime/src/platform/windows.rs @@ -0,0 +1,28 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +//! Windows implementation of `uptime`'s platform facade. There are no utmp +//! files on Windows, so the file-operand facades degenerate, and `--since` +//! reads the uptime directly from [`uucore::uptime::get_uptime`] +//! (`GetTickCount64`). + +use clap::{ArgMatches, Command}; +use uucore::error::UResult; +use uucore::uptime::get_uptime; + +/// No platform-only CLI arguments on Windows (no utmp files). +pub(crate) fn add_platform_args(cmd: Command) -> Command { + cmd +} + +/// The utmp file operand does not exist on Windows; never handled here. +pub(crate) fn maybe_uptime_from_file(_matches: &ArgMatches) -> Option> { + None +} + +/// The system uptime in seconds, for `--since`. +pub(crate) fn system_uptime_seconds() -> UResult { + get_uptime(None) +} diff --git a/src/uu/uptime/src/uptime.rs b/src/uu/uptime/src/uptime.rs index fe8144841a9..a5d248df1cd 100644 --- a/src/uu/uptime/src/uptime.rs +++ b/src/uu/uptime/src/uptime.rs @@ -3,15 +3,13 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore getloadavg behaviour loadavg uptime upsecs updays upmins uphours boottime nusers utmpxname gettime clockid couldnt +// spell-checker:ignore behaviour loadavg nusers + +mod platform; use clap::{Arg, ArgAction, Command}; -#[cfg(unix)] -use clap::{ValueHint, builder::ValueParser}; use jiff::tz::TimeZone; use jiff::{Timestamp, ToSpan}; -#[cfg(unix)] -use std::ffi::OsString; use std::io::{self, Write, stdout}; use thiserror::Error; use uucore::error::{UError, UResult}; @@ -20,13 +18,9 @@ use uucore::libc::time_t; use uucore::translate; use uucore::uptime::{ OutputFormat, format_nusers, get_formatted_loadavg, get_formatted_nusers, get_formatted_time, - get_formatted_uptime, get_uptime, + get_formatted_uptime, }; -#[cfg(unix)] -#[cfg(not(target_os = "openbsd"))] -use uucore::utmpx::{BOOT_TIME, USER_PROCESS, Utmpx}; - pub mod options { pub static SINCE: &str = "since"; pub static PATH: &str = "path"; @@ -54,20 +48,16 @@ impl UError for UptimeError { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; - #[cfg(unix)] - let file_path = matches.get_one::(options::PATH); - #[cfg(windows)] - let file_path = None; - if matches.get_flag(options::SINCE) { - uptime_since() - } else if matches.get_flag(options::PRETTY) { - pretty_print_uptime() - } else if let Some(path) = file_path { - uptime_with_file(path) - } else { - default_uptime() + return uptime_since(); } + if matches.get_flag(options::PRETTY) { + return pretty_print_uptime(); + } + if let Some(result) = platform::maybe_uptime_from_file(&matches) { + return result; + } + default_uptime() } pub fn uu_app() -> Command { @@ -88,128 +78,19 @@ pub fn uu_app() -> Command { .long(options::SINCE) .help(translate!("uptime-help-since")) .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(options::PRETTY) + .short('p') + .long(options::PRETTY) + .help(translate!("uptime-help-pretty")) + .action(ArgAction::SetTrue), ); - #[cfg(unix)] - cmd.arg( - Arg::new(options::PATH) - .help(translate!("uptime-help-path")) - .action(ArgAction::Set) - .num_args(0..=1) - .value_parser(ValueParser::os_string()) - .value_hint(ValueHint::AnyPath), - ) - .arg( - Arg::new(options::PRETTY) - .short('p') - .long(options::PRETTY) - .help(translate!("uptime-help-pretty")) - .action(ArgAction::SetTrue), - ) -} - -#[cfg(unix)] -fn uptime_with_file(file_path: &OsString) -> UResult<()> { - use std::fs; - use std::os::unix::fs::FileTypeExt; - use uucore::error::set_exit_code; - use uucore::show_error; - - // Uptime will print loadavg and time to stderr unless we encounter an extra operand. - let mut non_fatal_error = false; - - // process_utmpx_from_file() doesn't detect or report failures, we check if the path is valid - // before proceeding with more operations. - let md_res = fs::metadata(file_path); - if let Ok(md) = md_res { - if md.is_dir() { - show_error!("{}", UptimeError::TargetIsDir); - non_fatal_error = true; - set_exit_code(1); - } - if md.file_type().is_fifo() { - show_error!("{}", UptimeError::TargetIsFifo); - non_fatal_error = true; - set_exit_code(1); - } - } else if let Err(e) = md_res { - non_fatal_error = true; - set_exit_code(1); - show_error!("{}", UptimeError::IoErr(e)); - } - // utmpxname() returns an -1 , when filename doesn't end with 'x' or its too long. - // Reference: `` - - #[cfg(target_os = "macos")] - { - use std::os::unix::ffi::OsStrExt; - let bytes = file_path.as_os_str().as_bytes(); - - if bytes[bytes.len() - 1] != b'x' { - show_error!("{}", translate!("uptime-error-couldnt-get-boot-time")); - print_time()?; - write!(stdout(), "{}", translate!("uptime-output-unknown-uptime"))?; - print_nusers(Some(0))?; - print_loadavg()?; - set_exit_code(1); - return Ok(()); - } - } - - if non_fatal_error { - print_time()?; - write!(stdout(), "{}", translate!("uptime-output-unknown-uptime"))?; - print_nusers(Some(0))?; - print_loadavg()?; - return Ok(()); - } - - print_time()?; - let user_count; - - #[cfg(not(target_os = "openbsd"))] - { - let (boot_time, count) = process_utmpx(Some(file_path)); - if let Some(time) = boot_time { - print_uptime(Some(time))?; - } else { - show_error!("{}", translate!("uptime-error-couldnt-get-boot-time")); - set_exit_code(1); - - write!(stdout(), "{}", translate!("uptime-output-unknown-uptime"))?; - } - user_count = count; - } - - #[cfg(target_os = "openbsd")] - { - let upsecs = get_uptime(None)?; - if upsecs >= 0 { - print_uptime(Some(upsecs))?; - } else { - show_error!("{}", translate!("uptime-error-couldnt-get-boot-time")); - set_exit_code(1); - - write!(stdout(), "{}", translate!("uptime-output-unknown-uptime"))?; - } - user_count = - uucore::uptime::get_nusers(file_path.to_str().expect("invalid utmp path file")); - } - - print_nusers(Some(user_count))?; - print_loadavg()?; - - Ok(()) + platform::add_platform_args(cmd) } fn uptime_since() -> UResult<()> { - #[cfg(unix)] - #[cfg(not(target_os = "openbsd"))] - let uptime = { - let (boot_time, _) = process_utmpx(None); - get_uptime(boot_time)? - }; - #[cfg(any(windows, target_os = "openbsd"))] - let uptime = get_uptime(None)?; + let uptime = platform::system_uptime_seconds()?; let since_date = (Timestamp::now() - uptime.seconds()).to_zoned(TimeZone::system()); writeln!(stdout(), "{}", since_date.strftime("%Y-%m-%d %H:%M:%S"))?; @@ -227,44 +108,21 @@ fn default_uptime() -> UResult<()> { Ok(()) } +/// Prints the load average with its leading separator, or just the line ending +/// where load averages are unavailable (e.g. Windows), as GNU does. #[inline] fn print_loadavg() -> UResult<()> { if let Ok(s) = get_formatted_loadavg() { - writeln!(stdout(), "{s}")?; + write!(stdout(), ", {s}")?; } + writeln!(stdout())?; Ok(()) } -#[cfg(unix)] -#[cfg(not(target_os = "openbsd"))] -fn process_utmpx(file: Option<&OsString>) -> (Option, usize) { - let mut nusers = 0; - let mut boot_time = None; - - let records = match file { - Some(f) => Utmpx::iter_all_records_from(f), - None => Utmpx::iter_all_records(), - }; - - for line in records { - match line.record_type() { - x if x == USER_PROCESS => nusers += 1, - x if x == BOOT_TIME => { - let dt = line.login_time(); - if dt.unix_timestamp() > 0 { - boot_time = Some(dt.unix_timestamp() as time_t); - } - } - _ => (), - } - } - (boot_time, nusers) -} - fn print_nusers(nusers: Option) -> UResult<()> { write!( stdout(), - "{}, ", + "{}", match nusers { None => { get_formatted_nusers() diff --git a/src/uucore/src/lib/features/uptime/mod.rs b/src/uucore/src/lib/features/uptime/mod.rs new file mode 100644 index 00000000000..79538a2ebe0 --- /dev/null +++ b/src/uucore/src/lib/features/uptime/mod.rs @@ -0,0 +1,219 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore nusers loadavg + +//! Provides functions to get system uptime, number of users and load average. +//! +//! The platform-specific sources live in the cfg-gated `unix`/`windows` +//! submodules, which both provide `get_uptime`, `get_nusers`, `get_loadavg` +//! and `default_nusers` with identical signatures (one exception: OpenBSD's +//! `get_nusers` takes the utmp file path as an argument); the shared +//! formatting helpers here only talk to those re-exported functions. + +// The code was originally written in uu_uptime +// (https://github.com/uutils/coreutils/blob/main/src/uu/uptime/src/uptime.rs) +// but was eventually moved here. +// See https://github.com/uutils/coreutils/pull/7289 for discussion. + +use crate::error::{UError, UResult}; +use crate::translate; +use jiff::Timestamp; +use jiff::tz::TimeZone; +use libc::time_t; +use thiserror::Error; + +#[cfg(unix)] +mod unix; +#[cfg(unix)] +pub use unix::*; + +#[cfg(windows)] +mod windows; +#[cfg(windows)] +pub use windows::*; + +#[derive(Debug, Error)] +pub enum UptimeError { + #[error("{}", translate!("uptime-lib-error-system-uptime"))] + SystemUptime, + #[error("{}", translate!("uptime-lib-error-system-loadavg"))] + SystemLoadavg, + #[error("{}", translate!("uptime-lib-error-windows-loadavg"))] + WindowsLoadavg, + #[error("{}", translate!("uptime-lib-error-boot-time"))] + BootTime, +} + +impl UError for UptimeError { + fn code(&self) -> i32 { + 1 + } +} + +/// Returns the formatted time string, e.g. "12:34:56" +pub fn get_formatted_time() -> String { + Timestamp::now() + .to_zoned(TimeZone::system()) + .strftime("%H:%M:%S") + .to_string() +} + +/// The format used to display a FormattedUptime. +pub enum OutputFormat { + /// Typical `uptime` output (e.g. 2 days, 3:04). + HumanReadable, + + /// Pretty printed output (e.g. 2 days, 3 hours, 04 minutes). + PrettyPrint, +} + +struct FormattedUptime { + days: i64, + hours: i64, + mins: i64, +} + +impl FormattedUptime { + fn new(seconds: i64) -> Self { + let days = seconds / 86400; + let hours = (seconds - (days * 86400)) / 3600; + let mins = (seconds - (days * 86400) - (hours * 3600)) / 60; + + Self { days, hours, mins } + } + + fn get_human_readable_uptime(&self) -> String { + // Hours are not zero-padded (issue #13027); minutes always are. + translate!( + "uptime-format", + "days" => self.days, + "time" => format!("{}:{:02}", self.hours, self.mins)) + } + + fn get_pretty_print_uptime(&self) -> String { + let mut parts = Vec::new(); + if self.days > 0 { + parts.push(translate!("uptime-format-pretty-day", "day" => self.days)); + } + if self.hours > 0 { + parts.push(translate!("uptime-format-pretty-hour", "hour" => self.hours)); + } + if self.mins > 0 || parts.is_empty() { + parts.push(translate!("uptime-format-pretty-min", "min" => self.mins)); + } + parts.join(", ") + } +} + +/// Get the system uptime in a human-readable format +/// +/// # Arguments +/// +/// boot_time: Option - Manually specify the boot time, or None to try to get it from the system. +/// output_format: OutputFormat - Selects the format of the output string. +/// +/// # Returns +/// +/// Returns a UResult with the uptime in a human-readable format(e.g. "1 day, 3:45") if successful, otherwise an UptimeError. +#[inline] +pub fn get_formatted_uptime( + boot_time: Option, + output_format: OutputFormat, +) -> UResult { + let uptime = get_uptime(boot_time)?; + + if uptime < 0 { + Err(UptimeError::SystemUptime)?; + } + + let formatted_uptime = FormattedUptime::new(uptime); + + match output_format { + OutputFormat::HumanReadable => Ok(formatted_uptime.get_human_readable_uptime()), + OutputFormat::PrettyPrint => Ok(formatted_uptime.get_pretty_print_uptime()), + } +} + +/// Format the number of users to a human-readable string +/// +/// # Returns +/// +/// e.g. "0 users", "1 user", "2 users" +#[inline] +pub fn format_nusers(n: usize) -> String { + translate!( + "uptime-user-count", + "count" => n + ) +} + +/// Get the number of users currently logged in, in a human-readable format +/// +/// # Returns +/// +/// e.g. "0 user", "1 user", "2 users" +#[inline] +pub fn get_formatted_nusers() -> String { + format_nusers(default_nusers()) +} + +/// Get the system load average in a human-readable format +/// +/// # Returns +/// +/// Returns a UResult with the load average in a human-readable format if successful, otherwise an UptimeError. +/// e.g. "load average: 0.00, 0.00, 0.00" +#[inline] +pub fn get_formatted_loadavg() -> UResult { + let loadavg = get_loadavg()?; + let mut args = fluent::FluentArgs::new(); + args.set("avg1", format!("{:.2}", loadavg.0)); + args.set("avg5", format!("{:.2}", loadavg.1)); + args.set("avg15", format!("{:.2}", loadavg.2)); + Ok(crate::locale::get_message_with_args( + "uptime-lib-format-loadavg", + args, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::locale; + + #[test] + fn test_format_nusers() { + unsafe { + std::env::set_var("LANG", "en_US.UTF-8"); + } + let _ = locale::setup_localization("uptime"); + assert_eq!("0 users", format_nusers(0)); + assert_eq!("1 user", format_nusers(1)); + assert_eq!("2 users", format_nusers(2)); + } + + #[test] + fn test_human_readable_uptime_hours_not_zero_padded() { + unsafe { + std::env::set_var("LANG", "en_US.UTF-8"); + } + let _ = locale::setup_localization("uptime"); + // Hours below 10 are not zero-padded (issue #13027). + assert_eq!( + "1:27", + FormattedUptime::new(3600 + 27 * 60).get_human_readable_uptime() + ); + assert_eq!( + "9:05", + FormattedUptime::new(9 * 3600 + 5 * 60).get_human_readable_uptime() + ); + // Two-digit hours are unchanged. + assert_eq!( + "10:05", + FormattedUptime::new(10 * 3600 + 5 * 60).get_human_readable_uptime() + ); + } +} diff --git a/src/uucore/src/lib/features/uptime.rs b/src/uucore/src/lib/features/uptime/unix.rs similarity index 53% rename from src/uucore/src/lib/features/uptime.rs rename to src/uucore/src/lib/features/uptime/unix.rs index 3fab435ba3c..1e1bfa8c2b0 100644 --- a/src/uucore/src/lib/features/uptime.rs +++ b/src/uucore/src/lib/features/uptime/unix.rs @@ -3,47 +3,15 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore gettime BOOTTIME clockid boottime nusers loadavg getloadavg timeval +// spell-checker:ignore gettime BOOTTIME clockid boottime nusers loadavg getloadavg cfgs -//! Provides functions to get system uptime, number of users and load average. +//! Unix implementation of the platform side of `uucore::uptime`: system +//! uptime, user count and load average. The macOS/NetBSD/Cygwin and OpenBSD +//! variants live here as target cfgs. -// The code was originally written in uu_uptime -// (https://github.com/uutils/coreutils/blob/main/src/uu/uptime/src/uptime.rs) -// but was eventually moved here. -// See https://github.com/uutils/coreutils/pull/7289 for discussion. - -use crate::error::{UError, UResult}; -use crate::translate; -use jiff::Timestamp; -use jiff::tz::TimeZone; +use super::UptimeError; +use crate::error::UResult; use libc::time_t; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum UptimeError { - #[error("{}", translate!("uptime-lib-error-system-uptime"))] - SystemUptime, - #[error("{}", translate!("uptime-lib-error-system-loadavg"))] - SystemLoadavg, - #[error("{}", translate!("uptime-lib-error-windows-loadavg"))] - WindowsLoadavg, - #[error("{}", translate!("uptime-lib-error-boot-time"))] - BootTime, -} - -impl UError for UptimeError { - fn code(&self) -> i32 { - 1 - } -} - -/// Returns the formatted time string, e.g. "12:34:56" -pub fn get_formatted_time() -> String { - Timestamp::now() - .to_zoned(TimeZone::system()) - .strftime("%H:%M:%S") - .to_string() -} /// Safely get macOS boot time using sysctl command /// @@ -96,7 +64,6 @@ fn get_macos_boot_time_sysctl() -> Option { /// # Returns /// /// Returns a UResult with the uptime in seconds if successful, otherwise an UptimeError. -#[cfg(unix)] #[cfg(not(any(target_os = "cygwin", target_os = "netbsd", target_vendor = "apple")))] #[allow(clippy::unnecessary_wraps, reason = "needed on some platforms")] pub fn get_uptime(_boot_time: Option) -> UResult { @@ -120,6 +87,7 @@ pub fn get_uptime(_boot_time: Option) -> UResult { pub fn get_uptime(boot_time: Option) -> UResult { use crate::utmpx::BOOT_TIME; use crate::utmpx::Utmpx; + use jiff::Timestamp; use std::fs::File; use std::io::Read; @@ -179,106 +147,11 @@ pub fn get_uptime(boot_time: Option) -> UResult { Err(UptimeError::SystemUptime)? } -/// The format used to display a FormattedUptime. -pub enum OutputFormat { - /// Typical `uptime` output (e.g. 2 days, 3:04). - HumanReadable, - - /// Pretty printed output (e.g. 2 days, 3 hours, 04 minutes). - PrettyPrint, -} - -struct FormattedUptime { - days: i64, - hours: i64, - mins: i64, -} - -impl FormattedUptime { - fn new(seconds: i64) -> Self { - let days = seconds / 86400; - let hours = (seconds - (days * 86400)) / 3600; - let mins = (seconds - (days * 86400) - (hours * 3600)) / 60; - - Self { days, hours, mins } - } - - fn get_human_readable_uptime(&self) -> String { - // Hours are not zero-padded (issue #13027); minutes always are. - translate!( - "uptime-format", - "days" => self.days, - "time" => format!("{}:{:02}", self.hours, self.mins)) - } - - fn get_pretty_print_uptime(&self) -> String { - let mut parts = Vec::new(); - if self.days > 0 { - parts.push(translate!("uptime-format-pretty-day", "day" => self.days)); - } - if self.hours > 0 { - parts.push(translate!("uptime-format-pretty-hour", "hour" => self.hours)); - } - if self.mins > 0 || parts.is_empty() { - parts.push(translate!("uptime-format-pretty-min", "min" => self.mins)); - } - parts.join(", ") - } -} - -/// Get the system uptime -/// -/// # Arguments -/// -/// boot_time will be ignored, pass None. -/// -/// # Returns -/// -/// Returns a UResult with the uptime in seconds if successful, otherwise an UptimeError. -#[cfg(windows)] -#[allow(clippy::unnecessary_wraps, reason = "needed on some platforms")] -pub fn get_uptime(_boot_time: Option) -> UResult { - use windows_sys::Win32::System::SystemInformation::GetTickCount; - // SAFETY: always return u32 - let uptime = unsafe { GetTickCount() }; - Ok(uptime as i64 / 1000) -} - -/// Get the system uptime in a human-readable format -/// -/// # Arguments -/// -/// boot_time: Option - Manually specify the boot time, or None to try to get it from the system. -/// output_format: OutputFormat - Selects the format of the output string. -/// -/// # Returns -/// -/// Returns a UResult with the uptime in a human-readable format(e.g. "1 day, 3:45") if successful, otherwise an UptimeError. -#[inline] -pub fn get_formatted_uptime( - boot_time: Option, - output_format: OutputFormat, -) -> UResult { - let uptime = get_uptime(boot_time)?; - - if uptime < 0 { - Err(UptimeError::SystemUptime)?; - } - - let formatted_uptime = FormattedUptime::new(uptime); - - match output_format { - OutputFormat::HumanReadable => Ok(formatted_uptime.get_human_readable_uptime()), - OutputFormat::PrettyPrint => Ok(formatted_uptime.get_pretty_print_uptime()), - } -} - /// Get the number of users currently logged in /// /// # Returns /// /// Returns the number of users currently logged in if successful, otherwise 0. -#[cfg(unix)] #[cfg(not(target_os = "openbsd"))] // see: https://gitlab.com/procps-ng/procps/-/blob/4740a0efa79cade867cfc7b32955fe0f75bf5173/library/uptime.c#L63-L115 pub fn get_nusers() -> usize { @@ -321,93 +194,14 @@ pub fn get_nusers(file: &str) -> usize { .count() } -/// Get the number of users currently logged in -/// -/// # Returns -/// -/// Returns the number of users currently logged in if successful, otherwise 0 -#[cfg(target_os = "windows")] -pub fn get_nusers() -> usize { - use std::ptr; - use windows_sys::Win32::System::RemoteDesktop::{ - WTS_CURRENT_SERVER_HANDLE, WTSEnumerateSessionsW, WTSFreeMemory, - WTSQuerySessionInformationW, - }; - - let mut num_user = 0; - - // SAFETY: WTS_CURRENT_SERVER_HANDLE is a valid handle - unsafe { - let mut session_info_ptr = ptr::null_mut(); - let mut session_count = 0; - - let result = WTSEnumerateSessionsW( - WTS_CURRENT_SERVER_HANDLE, - 0, - 1, - &raw mut session_info_ptr, - &raw mut session_count, - ); - if result == 0 { - return 0; - } - - let sessions = std::slice::from_raw_parts(session_info_ptr, session_count as usize); - - for session in sessions { - let mut buffer: *mut u16 = ptr::null_mut(); - let mut bytes_returned = 0; - - let result = WTSQuerySessionInformationW( - WTS_CURRENT_SERVER_HANDLE, - session.SessionId, - 5, - &raw mut buffer, - &raw mut bytes_returned, - ); - if result == 0 || buffer.is_null() { - continue; - } - - let cstr = core::ffi::CStr::from_ptr(buffer.cast()); - if !cstr.is_empty() { - num_user += 1; - } - - WTSFreeMemory(buffer.cast()); - } - - WTSFreeMemory(session_info_ptr.cast()); - } - - num_user -} - -/// Format the number of users to a human-readable string -/// -/// # Returns -/// -/// e.g. "0 users", "1 user", "2 users" -#[inline] -pub fn format_nusers(n: usize) -> String { - translate!( - "uptime-user-count", - "count" => n - ) -} - -/// Get the number of users currently logged in, in a human-readable format -/// -/// # Returns -/// -/// e.g. "0 user", "1 user", "2 users" -#[inline] -pub fn get_formatted_nusers() -> String { +/// Get the number of users from the default system source, for +/// [`super::get_formatted_nusers`]. On OpenBSD the default source is +/// `/var/run/utmp`. +pub(crate) fn default_nusers() -> usize { #[cfg(not(target_os = "openbsd"))] - return format_nusers(get_nusers()); - + return get_nusers(); #[cfg(target_os = "openbsd")] - format_nusers(get_nusers("/var/run/utmp")) + get_nusers("/var/run/utmp") } /// Get the system load average @@ -416,7 +210,6 @@ pub fn get_formatted_nusers() -> String { /// /// Returns a UResult with the load average if successful, otherwise an UptimeError. /// The load average is a tuple of three floating point numbers representing the 1-minute, 5-minute, and 15-minute load averages. -#[cfg(unix)] pub fn get_loadavg() -> UResult<(f64, f64, f64)> { use core::ffi::c_double; use libc::getloadavg; @@ -432,78 +225,14 @@ pub fn get_loadavg() -> UResult<(f64, f64, f64)> { } } -/// Get the system load average -/// Windows does not have an equivalent to the load average on Unix-like systems. -/// -/// # Returns -/// -/// Returns a UResult with an UptimeError. -#[cfg(windows)] -pub fn get_loadavg() -> UResult<(f64, f64, f64)> { - Err(UptimeError::WindowsLoadavg)? -} - -/// Get the system load average in a human-readable format -/// -/// # Returns -/// -/// Returns a UResult with the load average in a human-readable format if successful, otherwise an UptimeError. -/// e.g. "load average: 0.00, 0.00, 0.00" -#[inline] -pub fn get_formatted_loadavg() -> UResult { - let loadavg = get_loadavg()?; - let mut args = fluent::FluentArgs::new(); - args.set("avg1", format!("{:.2}", loadavg.0)); - args.set("avg5", format!("{:.2}", loadavg.1)); - args.set("avg15", format!("{:.2}", loadavg.2)); - Ok(crate::locale::get_message_with_args( - "uptime-lib-format-loadavg", - args, - )) -} - -#[cfg(test)] +#[cfg(all(test, target_os = "macos"))] mod tests { use super::*; - use crate::locale; - - #[test] - fn test_format_nusers() { - unsafe { - std::env::set_var("LANG", "en_US.UTF-8"); - } - let _ = locale::setup_localization("uptime"); - assert_eq!("0 users", format_nusers(0)); - assert_eq!("1 user", format_nusers(1)); - assert_eq!("2 users", format_nusers(2)); - } - - #[test] - fn test_human_readable_uptime_hours_not_zero_padded() { - unsafe { - std::env::set_var("LANG", "en_US.UTF-8"); - } - let _ = locale::setup_localization("uptime"); - // Hours below 10 are not zero-padded (issue #13027). - assert_eq!( - "1:27", - FormattedUptime::new(3600 + 27 * 60).get_human_readable_uptime() - ); - assert_eq!( - "9:05", - FormattedUptime::new(9 * 3600 + 5 * 60).get_human_readable_uptime() - ); - // Two-digit hours are unchanged. - assert_eq!( - "10:05", - FormattedUptime::new(10 * 3600 + 5 * 60).get_human_readable_uptime() - ); - } + use jiff::Timestamp; /// Test that sysctl kern.boottime is accessible on macOS and returns valid boot time. /// This ensures the fallback mechanism added for issue #3621 works correctly. #[test] - #[cfg(target_os = "macos")] fn test_macos_sysctl_boottime_available() { // Test the safe wrapper function let boot_time = get_macos_boot_time_sysctl(); @@ -534,7 +263,6 @@ mod tests { /// Test that get_uptime always succeeds on macOS due to sysctl fallback. /// This addresses the intermittent failures reported in issue #3621. #[test] - #[cfg(target_os = "macos")] fn test_get_uptime_always_succeeds_on_macos() { // Call get_uptime without providing boot_time, forcing the system // to use utmpx or fall back to sysctl @@ -559,7 +287,6 @@ mod tests { /// Test get_uptime consistency by calling it multiple times. /// Verifies the sysctl fallback produces stable results. #[test] - #[cfg(target_os = "macos")] fn test_get_uptime_macos_consistency() { let uptime1 = get_uptime(None).expect("First call should succeed"); let uptime2 = get_uptime(None).expect("Second call should succeed"); diff --git a/src/uucore/src/lib/features/uptime/windows.rs b/src/uucore/src/lib/features/uptime/windows.rs new file mode 100644 index 00000000000..51bb22c6a38 --- /dev/null +++ b/src/uucore/src/lib/features/uptime/windows.rs @@ -0,0 +1,152 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore nusers loadavg INFOW + +//! Windows implementation of the platform side of `uucore::uptime`: +//! `GetTickCount64` for the uptime, WTS session enumeration for the user +//! count, and no load average. + +use super::UptimeError; +use crate::error::UResult; +use libc::time_t; +use std::ffi::OsString; +use std::os::windows::ffi::OsStringExt; +use std::ptr; +use windows_sys::Win32::System::RemoteDesktop::{ + WTS_CURRENT_SERVER_HANDLE, WTS_SESSION_INFOW, WTSEnumerateSessionsW, WTSFreeMemory, + WTSQuerySessionInformationW, WTSUserName, +}; + +/// Get the system uptime +/// +/// # Arguments +/// +/// boot_time will be ignored, pass None. +/// +/// # Returns +/// +/// Returns a UResult with the uptime in seconds if successful, otherwise an UptimeError. +#[allow(clippy::unnecessary_wraps, reason = "needed on some platforms")] +pub fn get_uptime(_boot_time: Option) -> UResult { + // GetTickCount64 (unlike GetTickCount) does not wrap after 49.7 days. + use windows_sys::Win32::System::SystemInformation::GetTickCount64; + // SAFETY: no preconditions; always returns milliseconds since boot + let uptime = unsafe { GetTickCount64() }; + Ok((uptime / 1000) as i64) +} + +/// Owns a WTS-allocated buffer, freeing it on every exit path. +struct WtsBuffer(*mut T); + +impl Drop for WtsBuffer { + fn drop(&mut self) { + // SAFETY: the pointer came from a successful WTS allocation and is + // freed exactly once, here. + unsafe { WTSFreeMemory(self.0.cast()) }; + } +} + +/// The sessions of the local server, enumerated once and freed on drop. +/// +/// Shaped for extraction into a shared module should another consumer appear +/// (e.g. a WTS-backed `users`). +struct Sessions { + buffer: WtsBuffer, + count: usize, +} + +impl Sessions { + /// Enumerate the sessions of the local server, or `None` if the WTS API + /// reports failure. + fn enumerate() -> Option { + let mut ptr = ptr::null_mut(); + let mut count = 0; + // SAFETY: WTS_CURRENT_SERVER_HANDLE is always valid and the two + // out-pointers are valid writable locations. + let result = unsafe { + WTSEnumerateSessionsW( + WTS_CURRENT_SERVER_HANDLE, + 0, + 1, + &raw mut ptr, + &raw mut count, + ) + }; + if result == 0 { + return None; + } + Some(Self { + buffer: WtsBuffer(ptr), + count: count as usize, + }) + } + + /// The session identifiers, in enumeration order. + fn ids(&self) -> impl Iterator { + // SAFETY: on success WTSEnumerateSessionsW produced an array of + // `count` entries, owned by `self.buffer` and freed only on drop. + let infos = unsafe { std::slice::from_raw_parts(self.buffer.0, self.count) }; + infos.iter().map(|info| info.SessionId) + } +} + +/// The user name a session is logged on as: `None` when the query fails, +/// `Some("")` when nobody is logged on to the session (services, listeners). +fn session_user_name(session_id: u32) -> Option { + let mut buffer: *mut u16 = ptr::null_mut(); + let mut byte_len = 0; + // SAFETY: WTS_CURRENT_SERVER_HANDLE is always valid and the two + // out-pointers are valid writable locations. + let result = unsafe { + WTSQuerySessionInformationW( + WTS_CURRENT_SERVER_HANDLE, + session_id, + WTSUserName, + &raw mut buffer, + &raw mut byte_len, + ) + }; + if result == 0 || buffer.is_null() { + return None; + } + let buffer = WtsBuffer(buffer); + // SAFETY: on success the buffer holds byte_len / 2 u16 units (the UTF-16 + // name including its terminating NUL), owned by `buffer` until drop. + let units = unsafe { std::slice::from_raw_parts(buffer.0, byte_len as usize / 2) }; + let name = units.split(|&unit| unit == 0).next().unwrap_or(&[]); + Some(OsString::from_wide(name)) +} + +/// Get the number of users currently logged in +/// +/// # Returns +/// +/// Returns the number of users currently logged in if successful, otherwise 0 +pub fn get_nusers() -> usize { + let Some(sessions) = Sessions::enumerate() else { + return 0; + }; + sessions + .ids() + .filter(|&id| session_user_name(id).is_some_and(|name| !name.is_empty())) + .count() +} + +/// Get the number of users from the default system source, for +/// [`super::get_formatted_nusers`]. +pub(crate) fn default_nusers() -> usize { + get_nusers() +} + +/// Get the system load average +/// Windows does not have an equivalent to the load average on Unix-like systems. +/// +/// # Returns +/// +/// Returns a UResult with an UptimeError. +pub fn get_loadavg() -> UResult<(f64, f64, f64)> { + Err(UptimeError::WindowsLoadavg)? +} diff --git a/tests/by-util/test_uptime.rs b/tests/by-util/test_uptime.rs index faf354d50fd..082d225bf39 100644 --- a/tests/by-util/test_uptime.rs +++ b/tests/by-util/test_uptime.rs @@ -6,7 +6,9 @@ // spell-checker:ignore utmp runlevel testusr testx boottime #![allow(clippy::cast_possible_wrap, clippy::unreadable_literal)] -use uutests::{at_and_ucmd, new_ucmd}; +#[cfg(unix)] +use uutests::at_and_ucmd; +use uutests::new_ucmd; use regex::Regex; @@ -17,12 +19,19 @@ fn test_invalid_arg() { #[test] fn test_uptime() { - new_ucmd!() - .succeeds() + let result = new_ucmd!().succeeds(); + result.stdout_contains(" up "); + // Don't check for users as it doesn't show in some CI + #[cfg(unix)] + result .stdout_contains("load average:") - .stdout_contains(" up "); + .stdout_does_not_contain(", ,"); - // Don't check for users as it doesn't show in some CI + // Windows has no load average; the line ends after the user count. + #[cfg(windows)] + result + .stdout_does_not_contain("load average") + .stdout_matches(&Regex::new(r" up .*, \d+ users?\n$").unwrap()); } #[test] @@ -42,6 +51,7 @@ fn test_write_error_handling() { /// Checks for files without utmpx records for which boot time cannot be calculated #[test] +#[cfg(unix)] #[cfg(not(any(target_os = "openbsd", target_os = "freebsd")))] // Disabled for freebsd, since it doesn't use the utmpxname() sys call to change the default utmpx // file that is accessed using getutxent() @@ -88,6 +98,7 @@ fn test_uptime_with_fifo() { } #[test] +#[cfg(unix)] #[cfg(not(target_os = "freebsd"))] fn test_uptime_with_non_existent_file() { // Disabled for freebsd, since it doesn't use the utmpxname() sys call to change the default utmpx @@ -102,6 +113,7 @@ fn test_uptime_with_non_existent_file() { // TODO create a similar test for macos // This will pass #[test] +#[cfg(unix)] #[cfg(not(any(target_os = "openbsd", target_os = "macos")))] #[cfg(not(target_env = "musl"))] #[cfg_attr( @@ -267,6 +279,7 @@ fn test_uptime_with_file_containing_valid_boot_time_utmpx_record() { } #[test] +#[cfg(unix)] fn test_uptime_with_extra_argument() { new_ucmd!() .arg("a") @@ -274,8 +287,20 @@ fn test_uptime_with_extra_argument() { .fails() .stderr_contains("unexpected value 'b'"); } + +/// The utmp file operand is unix-only; any operand is rejected on Windows. +#[test] +#[cfg(windows)] +fn test_uptime_with_file_windows() { + new_ucmd!() + .arg("file1") + .fails_with_code(1) + .stderr_contains("unexpected argument"); +} + /// Checks whether uptime displays the correct stderr msg when its called with a directory #[test] +#[cfg(unix)] fn test_uptime_with_dir() { let (at, mut ucmd) = at_and_ucmd!();