#[cfg(not(unix))]
fn main() {
eprintln!("uptime: only available on Unix");
std::process::exit(1);
}
#[cfg(unix)]
use std::process;
#[cfg(unix)]
const TOOL_NAME: &str = "uptime";
#[cfg(unix)]
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[cfg(unix)]
fn main() {
coreutils_rs::common::reset_sigpipe();
let mut pretty = false;
let mut since = false;
for arg in std::env::args().skip(1) {
match arg.as_str() {
"--help" => {
println!("Usage: {} [OPTION]...", TOOL_NAME);
println!("Print the current time, the length of time the system has been up,");
println!("the number of users on the system, and the average number of jobs");
println!("in the run queue over the last 1, 5 and 15 minutes.");
println!();
println!(" -p, --pretty show uptime in pretty format");
println!(" -s, --since system up since, in yyyy-mm-dd HH:MM:SS format");
println!(" --help display this help and exit");
println!(" --version output version information and exit");
return;
}
"--version" => {
println!("{} (fcoreutils) {}", TOOL_NAME, VERSION);
return;
}
"-p" | "--pretty" => pretty = true,
"-s" | "--since" => since = true,
s if s.starts_with('-') && s.len() > 1 && !s.starts_with("--") => {
for ch in s[1..].chars() {
match ch {
'p' => pretty = true,
's' => since = true,
_ => {
eprintln!("{}: invalid option -- '{}'", TOOL_NAME, ch);
eprintln!("Try '{} --help' for more information.", TOOL_NAME);
process::exit(1);
}
}
}
}
_ => {
eprintln!("{}: extra operand '{}'", TOOL_NAME, arg);
process::exit(1);
}
}
}
let uptime_secs = read_uptime().unwrap_or_else(|e| {
eprintln!("{}: {}", TOOL_NAME, e);
process::exit(1);
});
if since {
print_since(uptime_secs);
} else if pretty {
print_pretty(uptime_secs);
} else {
print_default(uptime_secs);
}
}
#[cfg(target_os = "linux")]
fn read_uptime() -> Result<f64, String> {
let content = std::fs::read_to_string("/proc/uptime")
.map_err(|e| format!("cannot read /proc/uptime: {}", e))?;
content
.split_whitespace()
.next()
.ok_or_else(|| "unexpected /proc/uptime format".to_string())?
.parse::<f64>()
.map_err(|_| "cannot parse uptime".to_string())
}
#[cfg(target_os = "macos")]
fn read_uptime() -> Result<f64, String> {
use std::mem;
use std::ptr;
let mut boottime: libc::timeval = unsafe { mem::zeroed() };
let mut size = mem::size_of::<libc::timeval>();
let mut mib: [libc::c_int; 2] = [libc::CTL_KERN, libc::KERN_BOOTTIME];
let ret = unsafe {
libc::sysctl(
mib.as_mut_ptr(),
2,
&mut boottime as *mut _ as *mut libc::c_void,
&mut size,
ptr::null_mut(),
0,
)
};
if ret != 0 {
return Err("cannot determine boot time".to_string());
}
let now = unsafe { libc::time(ptr::null_mut()) };
Ok((now - boottime.tv_sec) as f64)
}
#[cfg(unix)]
fn read_loadavg() -> (f64, f64, f64) {
let mut loadavg = [0.0f64; 3];
let ret = unsafe { libc::getloadavg(loadavg.as_mut_ptr(), 3) };
if ret == 3 {
(loadavg[0], loadavg[1], loadavg[2])
} else {
(0.0, 0.0, 0.0)
}
}
#[cfg(unix)]
fn count_users() -> usize {
let entries = coreutils_rs::who::read_utmpx_with_systemd_fallback_no_pid_check();
entries
.iter()
.filter(|e| e.ut_type == libc::USER_PROCESS)
.count()
}
#[cfg(unix)]
fn format_uptime(secs: f64) -> String {
let total_secs = secs as u64;
let days = total_secs / 86400;
let hours = (total_secs % 86400) / 3600;
let minutes = (total_secs % 3600) / 60;
if days > 0 {
if hours > 0 || minutes > 0 {
format!(
"{} day{}, {:2}:{:02}",
days,
if days != 1 { "s" } else { "" },
hours,
minutes
)
} else {
format!("{} day{}", days, if days != 1 { "s" } else { "" })
}
} else if hours > 0 {
format!("{:2}:{:02}", hours, minutes)
} else {
format!("{} min", minutes)
}
}
#[cfg(unix)]
fn print_default(uptime_secs: f64) {
let now = unsafe { libc::time(std::ptr::null_mut()) };
let tm = unsafe {
let mut tm: libc::tm = std::mem::zeroed();
libc::localtime_r(&now, &mut tm);
tm
};
let users = count_users();
let (l1, l5, l15) = read_loadavg();
let up_str = format_uptime(uptime_secs);
let user_str = if users == 1 { "user" } else { "users" };
println!(
" {:02}:{:02}:{:02} up {}, {} {}, load average: {:.2}, {:.2}, {:.2}",
tm.tm_hour, tm.tm_min, tm.tm_sec, up_str, users, user_str, l1, l5, l15
);
}
#[cfg(unix)]
fn print_pretty(uptime_secs: f64) {
let total_secs = uptime_secs as u64;
let days = total_secs / 86400;
let hours = (total_secs % 86400) / 3600;
let minutes = (total_secs % 3600) / 60;
let mut parts = Vec::new();
if days > 0 {
parts.push(format!("{} day{}", days, if days != 1 { "s" } else { "" }));
}
if hours > 0 {
parts.push(format!(
"{} hour{}",
hours,
if hours != 1 { "s" } else { "" }
));
}
if minutes > 0 {
parts.push(format!(
"{} minute{}",
minutes,
if minutes != 1 { "s" } else { "" }
));
}
if parts.is_empty() {
println!("up 0 minutes");
} else {
println!("up {}", parts.join(", "));
}
}
#[cfg(target_os = "linux")]
fn read_btime() -> Option<libc::time_t> {
use std::io::{BufRead, BufReader};
let f = std::fs::File::open("/proc/stat").ok()?;
for line in BufReader::new(f).lines().map_while(Result::ok) {
if let Some(rest) = line.strip_prefix("btime ") {
return rest.trim().parse::<libc::time_t>().ok();
}
}
None
}
#[cfg(unix)]
fn print_since(uptime_secs: f64) {
#[cfg(target_os = "linux")]
let boot_time = read_btime().unwrap_or_else(|| {
let now = unsafe { libc::time(std::ptr::null_mut()) };
now - uptime_secs.round() as libc::time_t
});
#[cfg(not(target_os = "linux"))]
let boot_time = {
let now = unsafe { libc::time(std::ptr::null_mut()) };
now - uptime_secs.round() as libc::time_t
};
let tm = unsafe {
let mut tm: libc::tm = std::mem::zeroed();
libc::localtime_r(&boot_time, &mut tm);
tm
};
println!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}",
tm.tm_year + 1900,
tm.tm_mon + 1,
tm.tm_mday,
tm.tm_hour,
tm.tm_min,
tm.tm_sec
);
}
#[cfg(all(test, unix))]
mod tests {
use std::process::Command;
fn cmd() -> Command {
let mut path = std::env::current_exe().unwrap();
path.pop();
path.pop();
path.push("fuptime");
Command::new(path)
}
#[test]
fn test_uptime_format() {
let output = cmd().output().unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("up"), "Should contain 'up'");
assert!(
stdout.contains("load average"),
"Should contain 'load average'"
);
}
#[test]
fn test_uptime_pretty() {
let output = cmd().arg("-p").output().unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.starts_with("up "),
"Pretty format should start with 'up '"
);
}
#[test]
fn test_uptime_since() {
let output = cmd().arg("-s").output().unwrap();
assert_eq!(output.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&output.stdout);
let trimmed = stdout.trim();
assert!(
trimmed.len() >= 19,
"Since format should be at least 19 chars: '{}'",
trimmed
);
assert!(trimmed.contains('-'), "Should contain date separator");
assert!(trimmed.contains(':'), "Should contain time separator");
}
#[test]
fn test_uptime_matches_gnu_format() {
let gnu = Command::new("uptime").arg("-p").output();
if let Ok(gnu) = gnu {
if !gnu.status.success() {
return;
}
let ours = cmd().arg("-p").output().unwrap();
assert_eq!(ours.status.code(), gnu.status.code(), "Exit code mismatch");
}
}
#[test]
fn test_uptime_basic() {
let output = cmd().output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("load average") || stdout.contains("up") || stdout.contains("user")
);
}
#[test]
fn test_uptime_pretty_contains_up() {
let output = cmd().arg("-p").output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("up"));
}
#[test]
fn test_uptime_since_datetime_format() {
let output = cmd().arg("-s").output().unwrap();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("-") && stdout.contains(":"));
}
}