Skip to content
65 changes: 65 additions & 0 deletions src/uucore/src/lib/features/uptime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
// See https://github.com/uutils/coreutils/pull/7289 for discussion.

use crate::error::{UError, UResult};
use crate::locale::{self, LocalizationError};
use crate::translate;
use jiff::Timestamp;
use jiff::tz::TimeZone;
use libc::time_t;
use std::cell::Cell;
use thiserror::Error;

#[derive(Debug, Error)]
Expand All @@ -37,6 +39,37 @@ impl UError for UptimeError {
}
}

// setup_localization() stores its state in thread local storage,
// so every thread that might format uptime strings has to call it once.
// Track that per thread so our helpers can lazily initialize
// the locale only once per thread.
thread_local! {
static LOCALE_READY: Cell<bool> = const { Cell::new(false) };
}

// Lazily initialize the uptime localization for the current thread, marking it
// done even if a sibling thread already ran setup_localization() so we avoid
// propagating the "already initialized" error and only log unexpected failures
// in debug builds.
fn ensure_uptime_locale() {
LOCALE_READY.with(|ready| {
if ready.get() {
return;
}

match locale::setup_localization("uptime") {
Ok(()) => ready.set(true),
Err(LocalizationError::Bundle(msg)) if msg.contains("already initialized") => {
ready.set(true);
}
Err(err) => {
#[cfg(debug_assertions)]
eprintln!("uucore::uptime localization setup failed: {err}");
}
}
});
}

/// Returns the formatted time string, e.g. "12:34:56"
pub fn get_formatted_time() -> String {
Timestamp::now()
Expand Down Expand Up @@ -98,6 +131,7 @@ fn get_macos_boot_time_sysctl() -> Option<time_t> {
/// Returns a UResult with the uptime in seconds if successful, otherwise an UptimeError.
#[cfg(target_os = "openbsd")]
pub fn get_uptime(_boot_time: Option<time_t>) -> UResult<i64> {
ensure_uptime_locale();
use libc::CLOCK_BOOTTIME;
use libc::clock_gettime;

Expand Down Expand Up @@ -136,6 +170,7 @@ pub fn get_uptime(_boot_time: Option<time_t>) -> UResult<i64> {
#[cfg(unix)]
#[cfg(not(target_os = "openbsd"))]
pub fn get_uptime(boot_time: Option<time_t>) -> UResult<i64> {
ensure_uptime_locale();
use crate::utmpx::Utmpx;
use libc::BOOT_TIME;
use std::fs::File;
Expand Down Expand Up @@ -266,6 +301,7 @@ impl FormattedUptime {
/// Returns a UResult with the uptime in seconds if successful, otherwise an UptimeError.
#[cfg(windows)]
pub fn get_uptime(_boot_time: Option<time_t>) -> UResult<i64> {
ensure_uptime_locale();
use windows_sys::Win32::System::SystemInformation::GetTickCount;
// SAFETY: always return u32
let uptime = unsafe { GetTickCount() };
Expand All @@ -287,6 +323,7 @@ pub fn get_formatted_uptime(
boot_time: Option<time_t>,
output_format: OutputFormat,
) -> UResult<String> {
ensure_uptime_locale();
let up_secs = get_uptime(boot_time)?;

if up_secs < 0 {
Expand Down Expand Up @@ -415,6 +452,7 @@ pub fn get_nusers() -> usize {
/// e.g. "0 users", "1 user", "2 users"
#[inline]
pub fn format_nusers(n: usize) -> String {
ensure_uptime_locale();
translate!(
"uptime-user-count",
"count" => n
Expand All @@ -428,6 +466,7 @@ pub fn format_nusers(n: usize) -> String {
/// e.g. "0 user", "1 user", "2 users"
#[inline]
pub fn get_formatted_nusers() -> String {
ensure_uptime_locale();
#[cfg(not(target_os = "openbsd"))]
return format_nusers(get_nusers());

Expand All @@ -443,6 +482,7 @@ pub fn get_formatted_nusers() -> String {
/// 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)> {
ensure_uptime_locale();
use crate::libc::c_double;
use libc::getloadavg;

Expand All @@ -465,6 +505,7 @@ pub fn get_loadavg() -> UResult<(f64, f64, f64)> {
/// Returns a UResult with an UptimeError.
#[cfg(windows)]
pub fn get_loadavg() -> UResult<(f64, f64, f64)> {
ensure_uptime_locale();
Err(UptimeError::WindowsLoadavg)?
}

Expand All @@ -476,6 +517,7 @@ pub fn get_loadavg() -> UResult<(f64, f64, f64)> {
/// e.g. "load average: 0.00, 0.00, 0.00"
#[inline]
pub fn get_formatted_loadavg() -> UResult<String> {
ensure_uptime_locale();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i am not a fan to duplicate this call in every functions of uptime
can we do it only at load time in one place ? thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. Unfortunately setup_localization uses a thread‑local LOCALIZER (see src/uucore/src/lib/mods/locale.rs), so initializing once at load time only affects that thread. If uptime is called from another thread, translate! returns the message id instead of a localized string. ensure_uptime_locale() is guarded by a thread‑local flag, so after the first call per thread it’s effectively a no‑op and avoids the “already initialized” error. This makes the API safe for multi‑threaded consumers (e.g. a refresh thread), while keeping the runtime cost minimal.

let loadavg = get_loadavg()?;
let mut args = fluent::FluentArgs::new();
args.set("avg1", format!("{:.2}", loadavg.0));
Expand Down Expand Up @@ -503,6 +545,29 @@ mod tests {
assert_eq!("2 users", format_nusers(2));
}

#[test]
fn test_format_nusers_threaded() {
unsafe {
std::env::set_var("LANG", "en_US.UTF-8");
}
let _ = locale::setup_localization("top");
let _ = locale::setup_localization("uptime");

assert_eq!("uptime-user-count", format_nusers(0));

std::thread::spawn(move || {
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));
})
.join()
.expect("thread should succeed");
}

/// 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]
Expand Down
Loading