From ccb4144b86ca069e10a91078f36472c33ac2febf Mon Sep 17 00:00:00 2001 From: Nikola Lukovic Date: Tue, 11 Aug 2026 11:51:35 +0200 Subject: [PATCH 1/6] uucore::uptime: fix GetTickCount wrap and UTF-16 user counting on Windows --- src/uucore/src/lib/features/uptime.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/uucore/src/lib/features/uptime.rs b/src/uucore/src/lib/features/uptime.rs index 3fab435ba3c..3a9234536f1 100644 --- a/src/uucore/src/lib/features/uptime.rs +++ b/src/uucore/src/lib/features/uptime.rs @@ -238,10 +238,11 @@ impl FormattedUptime { #[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) + // 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) } /// Get the system uptime in a human-readable format @@ -369,8 +370,10 @@ pub fn get_nusers() -> usize { continue; } - let cstr = core::ffi::CStr::from_ptr(buffer.cast()); - if !cstr.is_empty() { + // The buffer is UTF-16 (WTSUserNameW); checking it byte-wise as a + // C string would misread names whose first code unit has a zero + // low byte (e.g. U+AC00) as empty. + if *buffer != 0 { num_user += 1; } From 5426187be8278efcaa573dde4513fcbf34db795f Mon Sep 17 00:00:00 2001 From: Nikola Lukovic Date: Tue, 11 Aug 2026 11:51:41 +0200 Subject: [PATCH 2/6] uptime: enable on Windows --- Cargo.toml | 2 +- src/uu/uptime/src/uptime.rs | 48 +++++++++++++++++++----------------- tests/by-util/test_uptime.rs | 35 ++++++++++++++++++++++---- 3 files changed, 56 insertions(+), 29 deletions(-) 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/uptime.rs b/src/uu/uptime/src/uptime.rs index fe8144841a9..811c2dd9d9e 100644 --- a/src/uu/uptime/src/uptime.rs +++ b/src/uu/uptime/src/uptime.rs @@ -54,20 +54,17 @@ 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(); + } + #[cfg(unix)] + if let Some(path) = matches.get_one::(options::PATH) { + return uptime_with_file(path); + } + default_uptime() } pub fn uu_app() -> Command { @@ -88,23 +85,25 @@ 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), ); + // The utmp file operand only makes sense where utmp files exist. #[cfg(unix)] - cmd.arg( + let cmd = 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), - ) + ); + cmd } #[cfg(unix)] @@ -227,11 +226,14 @@ 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(()) } @@ -264,7 +266,7 @@ fn process_utmpx(file: Option<&OsString>) -> (Option, usize) { fn print_nusers(nusers: Option) -> UResult<()> { write!( stdout(), - "{}, ", + "{}", match nusers { None => { get_formatted_nusers() 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!(); From 6c2fa756142c26ce6f1b64475ca518eb672c4d89 Mon Sep 17 00:00:00 2001 From: Nikola Lukovic Date: Tue, 11 Aug 2026 12:20:52 +0200 Subject: [PATCH 3/6] uucore::uptime: move uptime.rs to uptime/mod.rs --- src/uucore/src/lib/features/{uptime.rs => uptime/mod.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/uucore/src/lib/features/{uptime.rs => uptime/mod.rs} (100%) diff --git a/src/uucore/src/lib/features/uptime.rs b/src/uucore/src/lib/features/uptime/mod.rs similarity index 100% rename from src/uucore/src/lib/features/uptime.rs rename to src/uucore/src/lib/features/uptime/mod.rs From 982ad798219bbaa7b5a6a91c1e8751dd203b1618 Mon Sep 17 00:00:00 2001 From: Nikola Lukovic Date: Tue, 11 Aug 2026 12:37:43 +0200 Subject: [PATCH 4/6] uptime: split platform-specific code into unix and windows submodules --- src/uu/uptime/src/platform/mod.rs | 21 + src/uu/uptime/src/platform/unix.rs | 175 ++++++++ src/uu/uptime/src/platform/windows.rs | 28 ++ src/uu/uptime/src/uptime.rs | 160 +------ src/uucore/src/lib/features/uptime/mod.rs | 394 +----------------- src/uucore/src/lib/features/uptime/unix.rs | 301 +++++++++++++ src/uucore/src/lib/features/uptime/windows.rs | 111 +++++ 7 files changed, 662 insertions(+), 528 deletions(-) create mode 100644 src/uu/uptime/src/platform/mod.rs create mode 100644 src/uu/uptime/src/platform/unix.rs create mode 100644 src/uu/uptime/src/platform/windows.rs create mode 100644 src/uucore/src/lib/features/uptime/unix.rs create mode 100644 src/uucore/src/lib/features/uptime/windows.rs 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 811c2dd9d9e..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"; @@ -60,9 +54,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { if matches.get_flag(options::PRETTY) { return pretty_print_uptime(); } - #[cfg(unix)] - if let Some(path) = matches.get_one::(options::PATH) { - return uptime_with_file(path); + if let Some(result) = platform::maybe_uptime_from_file(&matches) { + return result; } default_uptime() } @@ -93,122 +86,11 @@ pub fn uu_app() -> Command { .help(translate!("uptime-help-pretty")) .action(ArgAction::SetTrue), ); - // The utmp file operand only makes sense where utmp files exist. - #[cfg(unix)] - let cmd = 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), - ); - cmd -} - -#[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"))?; @@ -237,32 +119,6 @@ fn print_loadavg() -> UResult<()> { 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(), diff --git a/src/uucore/src/lib/features/uptime/mod.rs b/src/uucore/src/lib/features/uptime/mod.rs index 3a9234536f1..79538a2ebe0 100644 --- a/src/uucore/src/lib/features/uptime/mod.rs +++ b/src/uucore/src/lib/features/uptime/mod.rs @@ -3,9 +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 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) @@ -19,6 +25,16 @@ 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"))] @@ -45,140 +61,6 @@ pub fn get_formatted_time() -> String { .to_string() } -/// Safely get macOS boot time using sysctl command -/// -/// This function uses the sysctl command-line tool to retrieve the kernel -/// boot time on macOS, avoiding any unsafe code. It parses the output -/// of the sysctl command to extract the boot time. -/// -/// # Returns -/// -/// Returns Some(time_t) if successful, None if the call fails. -#[cfg(target_os = "macos")] -fn get_macos_boot_time_sysctl() -> Option { - use std::process::Command; - - // Execute sysctl command to get boot time - let output = Command::new("sysctl") - .arg("-n") - .arg("kern.boottime") - .output(); - - if let Ok(output) = output - && output.status.success() - { - // Parse output format: { sec = 1729338352, usec = 0 } Wed Oct 19 08:25:52 2025 - // We need to extract the seconds value from the structured output - let stdout = String::from_utf8_lossy(&output.stdout); - - // Extract the seconds from the output - // Look for "sec = " pattern - if let Some(sec_start) = stdout.find("sec = ") { - let sec_part = &stdout[sec_start + 6..]; - if let Some(sec_end) = sec_part.find(',') { - let sec_str = &sec_part[..sec_end]; - if let Ok(boot_time) = sec_str.trim().parse::() { - return Some(boot_time as time_t); - } - } - } - } - - None -} - -/// Get the system uptime -/// -/// # Arguments -/// -/// boot_time: Option - Manually specify the boot time, or None to try to get it from the system. -/// -/// # 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 { - use rustix::time::{ClockId, clock_gettime}; - - let tp = clock_gettime(ClockId::Boottime); - - Ok(tp.tv_sec as i64) -} - -/// Get the system uptime -/// -/// # Arguments -/// -/// boot_time: Option - Manually specify the boot time, or None to try to get it from the system. -/// -/// # Returns -/// -/// Returns a UResult with the uptime in seconds if successful, otherwise an UptimeError. -#[cfg(any(target_os = "cygwin", target_os = "netbsd", target_vendor = "apple"))] -pub fn get_uptime(boot_time: Option) -> UResult { - use crate::utmpx::BOOT_TIME; - use crate::utmpx::Utmpx; - use std::fs::File; - use std::io::Read; - - let mut proc_uptime_s = String::new(); - - let proc_uptime = File::open("/proc/uptime") - .ok() - .and_then(|mut f| f.read_to_string(&mut proc_uptime_s).ok()) - .and_then(|_| proc_uptime_s.split_whitespace().next()) - .and_then(|s| s.split('.').next().unwrap_or("0").parse::().ok()); - - if let Some(uptime) = proc_uptime { - return Ok(uptime); - } - - // Try provided boot_time or derive from utmpx - let derived_boot_time = boot_time.or_else(|| { - Utmpx::iter_all_records() - .filter(|r| r.record_type() == BOOT_TIME) - .map(|r| r.login_time().unix_timestamp()) - .find(|&ts| ts > 0) - .map(|ts| ts as time_t) - }); - - // macOS-specific fallback: use sysctl kern.boottime when utmpx did not provide BOOT_TIME - // - // On macOS, the utmpx BOOT_TIME record can be unreliable or absent, causing intermittent - // test failures (see issue #3621: https://github.com/uutils/coreutils/issues/3621). - // The sysctl(CTL_KERN, KERN_BOOTTIME) approach is the canonical way to retrieve boot time - // on macOS and is always available, making uptime more reliable on this platform. - // - // This fallback only runs if utmpx failed to provide a boot time. - #[cfg(target_os = "macos")] - let derived_boot_time = { - let mut t = derived_boot_time; - if t.is_none() { - // Use a safe wrapper function to get boot time via sysctl - if let Some(boot_time) = get_macos_boot_time_sysctl() { - t = Some(boot_time); - } - } - t - }; - - if let Some(t) = derived_boot_time { - let now = Timestamp::now().as_second(); - #[cfg(target_pointer_width = "64")] - let boottime: i64 = t; - #[cfg(not(target_pointer_width = "64"))] - let boottime: i64 = t.into(); - if now < boottime { - Err(UptimeError::BootTime)?; - } - return Ok(now - boottime); - } - - Err(UptimeError::SystemUptime)? -} - /// The format used to display a FormattedUptime. pub enum OutputFormat { /// Typical `uptime` output (e.g. 2 days, 3:04). @@ -226,25 +108,6 @@ impl FormattedUptime { } } -/// 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 { - // 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) -} - /// Get the system uptime in a human-readable format /// /// # Arguments @@ -274,118 +137,6 @@ pub fn get_formatted_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 { - use crate::utmpx::USER_PROCESS; - use crate::utmpx::Utmpx; - - let mut num_user = 0; - Utmpx::iter_all_records().for_each(|ut| { - if ut.record_type() == USER_PROCESS { - num_user += 1; - } - }); - num_user -} - -/// Get the number of users currently logged in -/// -/// # Returns -/// -/// Returns the number of users currently logged in if successful, otherwise 0 -#[cfg(target_os = "openbsd")] -pub fn get_nusers(file: &str) -> usize { - use utmp_classic::{UtmpEntry, parse_from_path}; - - let Ok(entries) = parse_from_path(file) else { - return 0; - }; - - if entries.is_empty() { - return 0; - } - - // Count entries that have a non-empty user field - entries - .iter() - .filter_map(|entry| match entry { - UtmpEntry::UTMP { user, .. } if !user.is_empty() => Some(()), - _ => None, - }) - .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; - } - - // The buffer is UTF-16 (WTSUserNameW); checking it byte-wise as a - // C string would misread names whose first code unit has a zero - // low byte (e.g. U+AC00) as empty. - if *buffer != 0 { - num_user += 1; - } - - WTSFreeMemory(buffer.cast()); - } - - WTSFreeMemory(session_info_ptr.cast()); - } - - num_user -} - /// Format the number of users to a human-readable string /// /// # Returns @@ -406,44 +157,7 @@ pub fn format_nusers(n: usize) -> String { /// e.g. "0 user", "1 user", "2 users" #[inline] pub fn get_formatted_nusers() -> String { - #[cfg(not(target_os = "openbsd"))] - return format_nusers(get_nusers()); - - #[cfg(target_os = "openbsd")] - format_nusers(get_nusers("/var/run/utmp")) -} - -/// Get the system load average -/// -/// # Returns -/// -/// 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; - - let mut avg: [c_double; 3] = [0.0; 3]; - // SAFETY: checked whether it returns -1 - let loads: i32 = unsafe { getloadavg(avg.as_mut_ptr(), 3) }; - - if loads == -1 { - Err(UptimeError::SystemLoadavg)? - } else { - Ok((avg[0], avg[1], avg[2])) - } -} - -/// 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)? + format_nusers(default_nusers()) } /// Get the system load average in a human-readable format @@ -502,76 +216,4 @@ mod tests { FormattedUptime::new(10 * 3600 + 5 * 60).get_human_readable_uptime() ); } - - /// 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(); - - // Verify the safe wrapper succeeded - assert!( - boot_time.is_some(), - "get_macos_boot_time_sysctl should succeed on macOS" - ); - - let boot_time = boot_time.unwrap(); - - // Verify boot time is valid (positive, reasonable value) - assert!(boot_time > 0, "Boot time should be positive"); - - // Boot time should be after 2000-01-01 (946684800 seconds since epoch) - assert!( - boot_time > 946_684_800, - "Boot time should be after year 2000" - ); - - // Boot time should be before current time - let boot_time = Timestamp::from_second(boot_time).unwrap(); - let now = Timestamp::now(); - assert!(boot_time < now, "Boot time should be before current time"); - } - - /// 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 - let result = get_uptime(None); - - assert!( - result.is_ok(), - "get_uptime should always succeed on macOS with sysctl fallback" - ); - - let uptime = result.unwrap(); - assert!(uptime > 0, "Uptime should be positive"); - - // Reasonable upper bound: system hasn't been up for more than 365 days - // (This is just a sanity check) - assert!( - uptime < 365 * 86400, - "Uptime seems unreasonably high: {uptime} seconds" - ); - } - - /// 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"); - - // Uptimes should be very close (within 1 second) - let diff = (uptime1 - uptime2).abs(); - assert!( - diff <= 1, - "Consecutive uptime calls should be consistent, got {uptime1} and {uptime2}" - ); - } } diff --git a/src/uucore/src/lib/features/uptime/unix.rs b/src/uucore/src/lib/features/uptime/unix.rs new file mode 100644 index 00000000000..9cc64d4a1fd --- /dev/null +++ b/src/uucore/src/lib/features/uptime/unix.rs @@ -0,0 +1,301 @@ +// 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 gettime BOOTTIME clockid boottime nusers loadavg getloadavg + +//! 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. + +use super::UptimeError; +use crate::error::UResult; +use libc::time_t; + +/// Safely get macOS boot time using sysctl command +/// +/// This function uses the sysctl command-line tool to retrieve the kernel +/// boot time on macOS, avoiding any unsafe code. It parses the output +/// of the sysctl command to extract the boot time. +/// +/// # Returns +/// +/// Returns Some(time_t) if successful, None if the call fails. +#[cfg(target_os = "macos")] +fn get_macos_boot_time_sysctl() -> Option { + use std::process::Command; + + // Execute sysctl command to get boot time + let output = Command::new("sysctl") + .arg("-n") + .arg("kern.boottime") + .output(); + + if let Ok(output) = output + && output.status.success() + { + // Parse output format: { sec = 1729338352, usec = 0 } Wed Oct 19 08:25:52 2025 + // We need to extract the seconds value from the structured output + let stdout = String::from_utf8_lossy(&output.stdout); + + // Extract the seconds from the output + // Look for "sec = " pattern + if let Some(sec_start) = stdout.find("sec = ") { + let sec_part = &stdout[sec_start + 6..]; + if let Some(sec_end) = sec_part.find(',') { + let sec_str = &sec_part[..sec_end]; + if let Ok(boot_time) = sec_str.trim().parse::() { + return Some(boot_time as time_t); + } + } + } + } + + None +} + +/// Get the system uptime +/// +/// # Arguments +/// +/// boot_time: Option - Manually specify the boot time, or None to try to get it from the system. +/// +/// # Returns +/// +/// Returns a UResult with the uptime in seconds if successful, otherwise an UptimeError. +#[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 { + use rustix::time::{ClockId, clock_gettime}; + + let tp = clock_gettime(ClockId::Boottime); + + Ok(tp.tv_sec as i64) +} + +/// Get the system uptime +/// +/// # Arguments +/// +/// boot_time: Option - Manually specify the boot time, or None to try to get it from the system. +/// +/// # Returns +/// +/// Returns a UResult with the uptime in seconds if successful, otherwise an UptimeError. +#[cfg(any(target_os = "cygwin", target_os = "netbsd", target_vendor = "apple"))] +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; + + let mut proc_uptime_s = String::new(); + + let proc_uptime = File::open("/proc/uptime") + .ok() + .and_then(|mut f| f.read_to_string(&mut proc_uptime_s).ok()) + .and_then(|_| proc_uptime_s.split_whitespace().next()) + .and_then(|s| s.split('.').next().unwrap_or("0").parse::().ok()); + + if let Some(uptime) = proc_uptime { + return Ok(uptime); + } + + // Try provided boot_time or derive from utmpx + let derived_boot_time = boot_time.or_else(|| { + Utmpx::iter_all_records() + .filter(|r| r.record_type() == BOOT_TIME) + .map(|r| r.login_time().unix_timestamp()) + .find(|&ts| ts > 0) + .map(|ts| ts as time_t) + }); + + // macOS-specific fallback: use sysctl kern.boottime when utmpx did not provide BOOT_TIME + // + // On macOS, the utmpx BOOT_TIME record can be unreliable or absent, causing intermittent + // test failures (see issue #3621: https://github.com/uutils/coreutils/issues/3621). + // The sysctl(CTL_KERN, KERN_BOOTTIME) approach is the canonical way to retrieve boot time + // on macOS and is always available, making uptime more reliable on this platform. + // + // This fallback only runs if utmpx failed to provide a boot time. + #[cfg(target_os = "macos")] + let derived_boot_time = { + let mut t = derived_boot_time; + if t.is_none() { + // Use a safe wrapper function to get boot time via sysctl + if let Some(boot_time) = get_macos_boot_time_sysctl() { + t = Some(boot_time); + } + } + t + }; + + if let Some(t) = derived_boot_time { + let now = Timestamp::now().as_second(); + #[cfg(target_pointer_width = "64")] + let boottime: i64 = t; + #[cfg(not(target_pointer_width = "64"))] + let boottime: i64 = t.into(); + if now < boottime { + Err(UptimeError::BootTime)?; + } + return Ok(now - boottime); + } + + Err(UptimeError::SystemUptime)? +} + +/// Get the number of users currently logged in +/// +/// # Returns +/// +/// Returns the number of users currently logged in if successful, otherwise 0. +#[cfg(not(target_os = "openbsd"))] +// see: https://gitlab.com/procps-ng/procps/-/blob/4740a0efa79cade867cfc7b32955fe0f75bf5173/library/uptime.c#L63-L115 +pub fn get_nusers() -> usize { + use crate::utmpx::USER_PROCESS; + use crate::utmpx::Utmpx; + + let mut num_user = 0; + Utmpx::iter_all_records().for_each(|ut| { + if ut.record_type() == USER_PROCESS { + num_user += 1; + } + }); + num_user +} + +/// Get the number of users currently logged in +/// +/// # Returns +/// +/// Returns the number of users currently logged in if successful, otherwise 0 +#[cfg(target_os = "openbsd")] +pub fn get_nusers(file: &str) -> usize { + use utmp_classic::{UtmpEntry, parse_from_path}; + + let Ok(entries) = parse_from_path(file) else { + return 0; + }; + + if entries.is_empty() { + return 0; + } + + // Count entries that have a non-empty user field + entries + .iter() + .filter_map(|entry| match entry { + UtmpEntry::UTMP { user, .. } if !user.is_empty() => Some(()), + _ => None, + }) + .count() +} + +/// 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 get_nusers(); + #[cfg(target_os = "openbsd")] + get_nusers("/var/run/utmp") +} + +/// Get the system load average +/// +/// # Returns +/// +/// 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. +pub fn get_loadavg() -> UResult<(f64, f64, f64)> { + use core::ffi::c_double; + use libc::getloadavg; + + let mut avg: [c_double; 3] = [0.0; 3]; + // SAFETY: checked whether it returns -1 + let loads: i32 = unsafe { getloadavg(avg.as_mut_ptr(), 3) }; + + if loads == -1 { + Err(UptimeError::SystemLoadavg)? + } else { + Ok((avg[0], avg[1], avg[2])) + } +} + +#[cfg(all(test, target_os = "macos"))] +mod tests { + use super::*; + 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] + fn test_macos_sysctl_boottime_available() { + // Test the safe wrapper function + let boot_time = get_macos_boot_time_sysctl(); + + // Verify the safe wrapper succeeded + assert!( + boot_time.is_some(), + "get_macos_boot_time_sysctl should succeed on macOS" + ); + + let boot_time = boot_time.unwrap(); + + // Verify boot time is valid (positive, reasonable value) + assert!(boot_time > 0, "Boot time should be positive"); + + // Boot time should be after 2000-01-01 (946684800 seconds since epoch) + assert!( + boot_time > 946_684_800, + "Boot time should be after year 2000" + ); + + // Boot time should be before current time + let boot_time = Timestamp::from_second(boot_time).unwrap(); + let now = Timestamp::now(); + assert!(boot_time < now, "Boot time should be before current time"); + } + + /// Test that get_uptime always succeeds on macOS due to sysctl fallback. + /// This addresses the intermittent failures reported in issue #3621. + #[test] + 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 + let result = get_uptime(None); + + assert!( + result.is_ok(), + "get_uptime should always succeed on macOS with sysctl fallback" + ); + + let uptime = result.unwrap(); + assert!(uptime > 0, "Uptime should be positive"); + + // Reasonable upper bound: system hasn't been up for more than 365 days + // (This is just a sanity check) + assert!( + uptime < 365 * 86400, + "Uptime seems unreasonably high: {uptime} seconds" + ); + } + + /// Test get_uptime consistency by calling it multiple times. + /// Verifies the sysctl fallback produces stable results. + #[test] + 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"); + + // Uptimes should be very close (within 1 second) + let diff = (uptime1 - uptime2).abs(); + assert!( + diff <= 1, + "Consecutive uptime calls should be consistent, got {uptime1} and {uptime2}" + ); + } +} 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..719aeef482d --- /dev/null +++ b/src/uucore/src/lib/features/uptime/windows.rs @@ -0,0 +1,111 @@ +// 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 + +//! 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; + +/// 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) +} + +/// 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 { + 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; + } + + // The buffer is UTF-16 (WTSUserNameW); checking it byte-wise as a + // C string would misread names whose first code unit has a zero + // low byte (e.g. U+AC00) as empty. + if *buffer != 0 { + num_user += 1; + } + + WTSFreeMemory(buffer.cast()); + } + + WTSFreeMemory(session_info_ptr.cast()); + } + + num_user +} + +/// 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)? +} From 079f7362415e5d897841b05466ca22ab6c61c4fe Mon Sep 17 00:00:00 2001 From: Nikola Lukovic Date: Tue, 11 Aug 2026 23:21:12 +0200 Subject: [PATCH 5/6] uptime: rewrite wts to be more safe to use --- src/uucore/src/lib/features/uptime/windows.rs | 145 +++++++++++------- 1 file changed, 93 insertions(+), 52 deletions(-) diff --git a/src/uucore/src/lib/features/uptime/windows.rs b/src/uucore/src/lib/features/uptime/windows.rs index 719aeef482d..c1dab97942b 100644 --- a/src/uucore/src/lib/features/uptime/windows.rs +++ b/src/uucore/src/lib/features/uptime/windows.rs @@ -12,6 +12,13 @@ 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 /// @@ -31,67 +38,101 @@ pub fn get_uptime(_boot_time: Option) -> UResult { Ok((uptime / 1000) as i64) } -/// 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 { - use std::ptr; - use windows_sys::Win32::System::RemoteDesktop::{ - WTS_CURRENT_SERVER_HANDLE, WTSEnumerateSessionsW, WTSFreeMemory, - WTSQuerySessionInformationW, - }; +/// Owns a WTS-allocated buffer, freeing it on every exit path. +struct WtsBuffer(*mut T); - let mut num_user = 0; +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()) }; + } +} - // SAFETY: WTS_CURRENT_SERVER_HANDLE is a valid handle - unsafe { - let mut session_info_ptr = ptr::null_mut(); - let mut session_count = 0; +/// 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, +} - let result = WTSEnumerateSessionsW( - WTS_CURRENT_SERVER_HANDLE, - 0, - 1, - &raw mut session_info_ptr, - &raw mut session_count, - ); +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 0; + return None; } + Some(Self { + buffer: WtsBuffer(ptr), + count: count as usize, + }) + } - 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; - } - - // The buffer is UTF-16 (WTSUserNameW); checking it byte-wise as a - // C string would misread names whose first code unit has a zero - // low byte (e.g. U+AC00) as empty. - if *buffer != 0 { - num_user += 1; - } - - WTSFreeMemory(buffer.cast()); - } + /// 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) + } +} - WTSFreeMemory(session_info_ptr.cast()); +/// 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)) +} - num_user +/// 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 From a6c5298930b75fefc79f32c0b8f5eaf25409f0c7 Mon Sep 17 00:00:00 2001 From: Nikola Lukovic Date: Tue, 11 Aug 2026 23:26:23 +0200 Subject: [PATCH 6/6] uptime: cspell: add missing words to spell checker --- src/uucore/src/lib/features/uptime/unix.rs | 2 +- src/uucore/src/lib/features/uptime/windows.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/uucore/src/lib/features/uptime/unix.rs b/src/uucore/src/lib/features/uptime/unix.rs index 9cc64d4a1fd..1e1bfa8c2b0 100644 --- a/src/uucore/src/lib/features/uptime/unix.rs +++ b/src/uucore/src/lib/features/uptime/unix.rs @@ -3,7 +3,7 @@ // 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 +// spell-checker:ignore gettime BOOTTIME clockid boottime nusers loadavg getloadavg cfgs //! Unix implementation of the platform side of `uucore::uptime`: system //! uptime, user count and load average. The macOS/NetBSD/Cygwin and OpenBSD diff --git a/src/uucore/src/lib/features/uptime/windows.rs b/src/uucore/src/lib/features/uptime/windows.rs index c1dab97942b..51bb22c6a38 100644 --- a/src/uucore/src/lib/features/uptime/windows.rs +++ b/src/uucore/src/lib/features/uptime/windows.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore nusers loadavg +// 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