use crate::error::{UError, UResult};
use crate::translate;
use jiff::Timestamp;
use jiff::tz::TimeZone;
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
}
}
pub fn get_formatted_time() -> String {
Timestamp::now()
.to_zoned(TimeZone::system())
.strftime("%H:%M:%S")
.to_string()
}
#[cfg(target_os = "macos")]
fn get_macos_boot_time_sysctl() -> Option<time_t> {
use std::process::Command;
let output = Command::new("sysctl")
.arg("-n")
.arg("kern.boottime")
.output();
if let Ok(output) = output {
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
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::<i64>() {
return Some(boot_time as time_t);
}
}
}
}
}
None
}
#[cfg(target_os = "openbsd")]
pub fn get_uptime(_boot_time: Option<time_t>) -> UResult<i64> {
use libc::CLOCK_BOOTTIME;
use libc::clock_gettime;
use libc::c_int;
use libc::timespec;
let mut tp: timespec = timespec {
tv_sec: 0,
tv_nsec: 0,
};
let ret: c_int = unsafe { clock_gettime(CLOCK_BOOTTIME, &raw mut tp) };
if ret == 0 {
#[cfg(target_pointer_width = "64")]
let uptime: i64 = tp.tv_sec;
#[cfg(not(target_pointer_width = "64"))]
let uptime: i64 = tp.tv_sec.into();
Ok(uptime)
} else {
Err(UptimeError::SystemUptime)?
}
}
#[cfg(unix)]
#[cfg(not(target_os = "openbsd"))]
pub fn get_uptime(boot_time: Option<time_t>) -> UResult<i64> {
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::<i64>().ok());
if let Some(uptime) = proc_uptime {
return Ok(uptime);
}
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)
});
#[cfg(target_os = "macos")]
let derived_boot_time = {
let mut t = derived_boot_time;
if t.is_none() {
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)?
}
pub enum OutputFormat {
HumanReadable,
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 {
translate!(
"uptime-format",
"days" => self.days,
"time" => format!("{:02}:{: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(", ")
}
}
#[cfg(windows)]
pub fn get_uptime(_boot_time: Option<time_t>) -> UResult<i64> {
use windows_sys::Win32::System::SystemInformation::GetTickCount;
let uptime = unsafe { GetTickCount() };
Ok(uptime as i64 / 1000)
}
#[inline]
pub fn get_formatted_uptime(
boot_time: Option<time_t>,
output_format: OutputFormat,
) -> UResult<String> {
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()),
}
}
#[cfg(unix)]
#[cfg(not(target_os = "openbsd"))]
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
}
#[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;
}
entries
.iter()
.filter_map(|entry| match entry {
UtmpEntry::UTMP { user, .. } if !user.is_empty() => Some(()),
_ => None,
})
.count()
}
#[cfg(target_os = "windows")]
pub fn get_nusers() -> usize {
use std::ptr;
use windows_sys::Win32::System::RemoteDesktop::*;
let mut num_user = 0;
unsafe {
let mut session_info_ptr = ptr::null_mut();
let mut session_count = 0;
let result = WTSEnumerateSessionsW(
WTS_CURRENT_SERVER_HANDLE,
0,
1,
&mut session_info_ptr,
&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,
&mut buffer,
&mut bytes_returned,
);
if result == 0 || buffer.is_null() {
continue;
}
let cstr = std::ffi::CStr::from_ptr(buffer.cast());
if !cstr.is_empty() {
num_user += 1;
}
WTSFreeMemory(buffer as _);
}
WTSFreeMemory(session_info_ptr as _);
}
num_user
}
#[inline]
pub fn format_nusers(n: usize) -> String {
translate!(
"uptime-user-count",
"count" => n
)
}
#[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"))
}
#[cfg(unix)]
pub fn get_loadavg() -> UResult<(f64, f64, f64)> {
use crate::libc::c_double;
use libc::getloadavg;
let mut avg: [c_double; 3] = [0.0; 3];
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(windows)]
pub fn get_loadavg() -> UResult<(f64, f64, f64)> {
Err(UptimeError::WindowsLoadavg)?
}
#[inline]
pub fn get_formatted_loadavg() -> UResult<String> {
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]
#[cfg(target_os = "macos")]
fn test_macos_sysctl_boottime_available() {
let boot_time = get_macos_boot_time_sysctl();
assert!(
boot_time.is_some(),
"get_macos_boot_time_sysctl should succeed on macOS"
);
let boot_time = boot_time.unwrap();
assert!(boot_time > 0, "Boot time should be positive");
assert!(
boot_time > 946_684_800,
"Boot time should be after year 2000"
);
let now = Timestamp::now().as_second();
assert!(
(boot_time as i64) < now,
"Boot time should be before current time"
);
}
#[test]
#[cfg(target_os = "macos")]
fn test_get_uptime_always_succeeds_on_macos() {
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");
assert!(
uptime < 365 * 86400,
"Uptime seems unreasonably high: {uptime} seconds"
);
}
#[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");
let diff = (uptime1 - uptime2).abs();
assert!(
diff <= 1,
"Consecutive uptime calls should be consistent, got {uptime1} and {uptime2}"
);
}
}