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

use crate::error::{UError, UResult};
use crate::locale::{self, LocalizationError};
use crate::translate;
use chrono::Local;
use libc::time_t;
use std::cell::Cell;
use thiserror::Error;

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

thread_local! {
static LOCALE_READY: Cell<bool> = const { Cell::new(false) };
}

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 {
Local::now().time().format("%H:%M:%S").to_string()
Expand All @@ -52,6 +77,7 @@ pub fn get_formatted_time() -> String {
/// 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 @@ -91,6 +117,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 @@ -150,6 +177,7 @@ pub fn get_uptime(boot_time: Option<time_t>) -> UResult<i64> {
/// 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 @@ -167,6 +195,7 @@ pub fn get_uptime(_boot_time: Option<time_t>) -> UResult<i64> {
/// 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<time_t>) -> UResult<String> {
ensure_uptime_locale();
let up_secs = get_uptime(boot_time)?;

if up_secs < 0 {
Expand Down Expand Up @@ -307,6 +336,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 @@ -320,6 +350,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 @@ -335,6 +366,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 @@ -357,6 +389,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 @@ -368,6 +401,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()?;
Ok(translate!(
"uptime-lib-format-loadavg",
Expand All @@ -392,4 +426,24 @@ mod tests {
assert_eq!("1 user", format_nusers(1));
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 || {
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");
}
}
Loading