mirror of
https://github.com/uutils/coreutils.git
synced 2026-05-06 07:26:38 -04:00
Fix uptime on macOS using sysctl kern.boottime fallback (#8908)
* fix(uucore): use sysctl kern.boottime on macOS as fallback for uptime If utmpx BOOT_TIME is unavailable, derive boot time via sysctl CTL_KERN.KERN_BOOTTIME to reduce intermittent macOS failures (e.g., #3621). Context (blame/history): -2774274cc2("uptime: Support files in uptime (#6400)"): added macOS utmpxname validation and non-fatal 'unknown uptime' fallback with tests (tests/by-util/test_uptime.rs). -920d29f703("uptime: add support for OpenBSD using utmp"): reorganized uptime.rs and solidified utmp/utmpx-driven paths. * test: add comprehensive macOS tests for sysctl kern.boottime fallback Add unit tests for sysctl boottime availability and get_uptime reliability on macOS, verifying the fallback mechanism works correctly when utmpx BOOT_TIME is unavailable. Add integration tests to ensure uptime command consistently succeeds on macOS with various flags (default, --since) and produces properly formatted output. Enhance documentation of the sysctl fallback code with detailed comments explaining why it exists, the issue it addresses (#3621), and comprehensive SAFETY comments for the unsafe sysctl call. All tests are properly gated with #[cfg(target_os = "macos")] to ensure they only run on macOS and don't interfere with other platforms. * refactor(uucore): replace unsafe sysctl with safe command-line approach for macOS boot time - Remove unsafe libc::sysctl() system call entirely - Replace with safe std::process::Command executing 'sysctl -n kern.boottime' - Parse sysctl output format to extract boot time seconds - Maintains same API and functionality while eliminating unsafe blocks - Addresses reviewer feedback to completely remove unsafe code
This commit is contained in:
committed by
GitHub
parent
6c5fae7a48
commit
3e71f638bc
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -107,7 +149,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() {
|
||||
@@ -123,7 +166,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;
|
||||
@@ -386,4 +449,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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
use uutests::at_and_ucmd;
|
||||
@@ -269,3 +269,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}"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user