Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
143 changes: 140 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 @@ -41,6 +41,48 @@ pub fn get_formatted_time() -> String {
Local::now().time().format("%H:%M:%S").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<time_t> {
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 {
if 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::<i64>() {
return Some(boot_time as time_t);
}
}
}
}
}

None
}

/// Get the system uptime
///
/// # Arguments
Expand Down Expand Up @@ -108,7 +150,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 +167,27 @@ 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 = {
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 = Local::now().timestamp();
#[cfg(target_pointer_width = "64")]
let boottime: i64 = t;
Expand Down Expand Up @@ -392,4 +455,78 @@ 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() {
// 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 > 946684800, "Boot time should be after year 2000");

// Boot time should be before current time
let now = chrono::Local::now().timestamp();
assert!(
(boot_time 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