Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 145 additions & 3 deletions src/uucore/src/lib/features/uptime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 timeval

//! Provides functions to get system uptime, number of users and load average.

Expand Down Expand Up @@ -108,7 +108,8 @@ pub fn get_uptime(boot_time: Option<time_t>) -> UResult<i64> {
return Ok(uptime);
}

let boot_time = boot_time.or_else(|| {
// Try provided boot_time or derive from utmpx
let derived_boot_time = boot_time.or_else(|| {
let records = Utmpx::iter_all_records();
for line in records {
match line.record_type() {
Expand All @@ -124,7 +125,54 @@ pub fn get_uptime(boot_time: Option<time_t>) -> UResult<i64> {
None
});

if let Some(t) = boot_time {
// 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 = {
use libc::{c_int, c_void, size_t, sysctl, timeval};
use std::mem::size_of;
use std::ptr;

let mut t = derived_boot_time;
if t.is_none() {
// MIB for kern.boottime - the macOS-specific way to get boot time
let mut mib: [c_int; 2] = [libc::CTL_KERN, libc::KERN_BOOTTIME];
let mut tv: timeval = timeval {
tv_sec: 0,
tv_usec: 0,
};
let mut tv_len: size_t = size_of::<timeval>() as size_t;

// SAFETY: We're calling sysctl with valid parameters:
// - mib is a valid 2-element array for kern.boottime
// - tv is a properly initialized timeval struct
// - tv_len correctly reflects the size of tv
// - All pointers are valid and properly aligned
// - We check the return value before using the result
let ret = unsafe {
sysctl(
mib.as_mut_ptr(),
2u32, // namelen
std::ptr::from_mut::<timeval>(&mut tv) as *mut c_void,
std::ptr::from_mut::<size_t>(&mut tv_len),
ptr::null_mut(),
0,
)
};
if ret == 0 && tv.tv_sec > 0 {
t = Some(tv.tv_sec as time_t);
}
}
t
};

if let Some(t) = derived_boot_time {
let now = Local::now().timestamp();
#[cfg(target_pointer_width = "64")]
let boottime: i64 = t;
Expand Down Expand Up @@ -392,4 +440,98 @@ mod tests {
assert_eq!("1 user", format_nusers(1));
assert_eq!("2 users", format_nusers(2));
}

/// 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() {
use libc::{c_int, c_void, size_t, sysctl, timeval};
use std::mem::size_of;
use std::ptr;

// Attempt to get boot time directly via sysctl
let mut mib: [c_int; 2] = [libc::CTL_KERN, libc::KERN_BOOTTIME];
let mut tv: timeval = timeval {
tv_sec: 0,
tv_usec: 0,
};
let mut tv_len: size_t = size_of::<timeval>() as size_t;

// SAFETY: We're calling sysctl with valid parameters:
// - mib is a valid 2-element array for kern.boottime
// - tv is a properly initialized timeval struct
// - tv_len correctly reflects the size of tv
// - We check the return value before using the result
let ret = unsafe {
sysctl(
mib.as_mut_ptr(),
2u32,
std::ptr::from_mut::<timeval>(&mut tv) as *mut c_void,
std::ptr::from_mut::<size_t>(&mut tv_len),
ptr::null_mut(),
0,
)
};

// Verify sysctl succeeded
assert_eq!(ret, 0, "sysctl kern.boottime should succeed on macOS");

// Verify boot time is valid (positive, reasonable value)
assert!(tv.tv_sec > 0, "Boot time should be positive");

// Boot time should be after 2000-01-01 (946684800 seconds since epoch)
assert!(tv.tv_sec > 946684800, "Boot time should be after year 2000");

// Boot time should be before current time
let now = chrono::Local::now().timestamp();
assert!(
(tv.tv_sec as i64) < 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: {} seconds",
uptime
);
}

/// 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 {} and {}",
uptime1,
uptime2
);
}
}
78 changes: 77 additions & 1 deletion tests/by-util/test_uptime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 bincode serde utmp runlevel testusr testx
// spell-checker:ignore bincode serde utmp runlevel testusr testx boottime
#![allow(clippy::cast_possible_wrap, clippy::unreadable_literal)]

#[cfg(not(target_os = "openbsd"))]
Expand Down Expand Up @@ -270,3 +270,79 @@ fn test_uptime_since() {

new_ucmd!().arg("--since").succeeds().stdout_matches(&re);
}

/// Test uptime reliability on macOS with sysctl kern.boottime fallback.
/// This addresses intermittent failures from issue #3621 by ensuring
/// the command consistently succeeds when utmpx data is unavailable.
#[test]
#[cfg(target_os = "macos")]
fn test_uptime_macos_reliability() {
// Run uptime multiple times to ensure consistent success
// (Previously would fail intermittently when utmpx had no BOOT_TIME)
for i in 0..5 {
let result = new_ucmd!().succeeds();

// Verify standard output patterns
result
.stdout_contains("up")
.stdout_contains("load average:");

// Ensure no error about retrieving system uptime
let stderr = result.stderr_str();
assert!(
!stderr.contains("could not retrieve system uptime"),
"Iteration {i}: uptime should not fail on macOS (stderr: {stderr})"
);
}
}

/// Test uptime --since reliability on macOS.
/// Verifies the sysctl fallback works for the --since flag.
#[test]
#[cfg(target_os = "macos")]
fn test_uptime_since_macos() {
let re = Regex::new(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}").unwrap();

// Run multiple times to ensure consistency
for i in 0..3 {
let result = new_ucmd!().arg("--since").succeeds();

result.stdout_matches(&re);

// Ensure no error messages
let stderr = result.stderr_str();
assert!(
stderr.is_empty(),
"Iteration {i}: uptime --since should not produce stderr on macOS (stderr: {stderr})"
);
}
}

/// Test that uptime output format is consistent on macOS.
/// Ensures the sysctl fallback produces properly formatted output.
#[test]
#[cfg(target_os = "macos")]
fn test_uptime_macos_output_format() {
let result = new_ucmd!().succeeds();
let stdout = result.stdout_str();

// Verify time is present (format: HH:MM:SS)
let time_re = Regex::new(r"\d{2}:\d{2}:\d{2}").unwrap();
assert!(
time_re.is_match(stdout),
"Output should contain time in HH:MM:SS format: {stdout}"
);

// Verify uptime format (either "HH:MM" or "X days HH:MM")
assert!(
stdout.contains(" up "),
"Output should contain 'up': {stdout}"
);

// Verify load average is present
let load_re = Regex::new(r"load average: \d+\.\d+, \d+\.\d+, \d+\.\d+").unwrap();
assert!(
load_re.is_match(stdout),
"Output should contain load average: {stdout}"
);
}
Loading