-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathuptime.rs
More file actions
641 lines (558 loc) · 19 KB
/
uptime.rs
File metadata and controls
641 lines (558 loc) · 19 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
// This file is part of the uutils coreutils package.
//
// 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 timeval
//! Provides functions to get system uptime, number of users and load average.
// The code was originally written in uu_uptime
// (https://github.com/uutils/coreutils/blob/main/src/uu/uptime/src/uptime.rs)
// but was eventually moved here.
// See https://github.com/uutils/coreutils/pull/7289 for discussion.
use crate::error::{UError, UResult};
use crate::locale::{self, LocalizationError};
use crate::translate;
use jiff::Timestamp;
use jiff::tz::TimeZone;
use libc::time_t;
use std::cell::Cell;
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
}
}
// setup_localization() stores its state in thread local storage,
// so every thread that might format uptime strings has to call it once.
// Track that per thread so our helpers can lazily initialize
// the locale only once per thread.
thread_local! {
static LOCALE_READY: Cell<bool> = const { Cell::new(false) };
}
// Lazily initialize the uptime localization for the current thread, marking it
// done even if a sibling thread already ran setup_localization() so we avoid
// propagating the "already initialized" error and only log unexpected failures
// in debug builds.
fn ensure_uptime_locale() {
LOCALE_READY.with(|ready| {
if ready.get() {
return;
}
match locale::setup_localization("uptime") {
Ok(()) => ready.set(true),
Err(LocalizationError::Bundle(msg)) if msg.contains("already initialized") => {
ready.set(true);
}
Err(err) => {
#[cfg(debug_assertions)]
eprintln!("uucore::uptime localization setup failed: {err}");
}
}
});
}
/// Returns the formatted time string, e.g. "12:34:56"
pub fn get_formatted_time() -> String {
Timestamp::now()
.to_zoned(TimeZone::system())
.strftime("%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
///
/// boot_time: Option<time_t> - Manually specify the boot time, or None to try to get it from the system.
///
/// # Returns
///
/// Returns a UResult with the uptime in seconds if successful, otherwise an UptimeError.
#[cfg(target_os = "openbsd")]
pub fn get_uptime(_boot_time: Option<time_t>) -> UResult<i64> {
ensure_uptime_locale();
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,
};
// OpenBSD prototype: clock_gettime(clk_id: ::clockid_t, tp: *mut ::timespec) -> ::c_int;
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)?
}
}
/// Get the system uptime
///
/// # Arguments
///
/// boot_time: Option<time_t> - Manually specify the boot time, or None to try to get it from the system.
///
/// # Returns
///
/// Returns a UResult with the uptime in seconds if successful, otherwise an UptimeError.
#[cfg(unix)]
#[cfg(not(target_os = "openbsd"))]
pub fn get_uptime(boot_time: Option<time_t>) -> UResult<i64> {
ensure_uptime_locale();
use crate::utmpx::Utmpx;
use libc::BOOT_TIME;
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);
}
// 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() {
BOOT_TIME => {
let dt = line.login_time();
if dt.unix_timestamp() > 0 {
return Some(dt.unix_timestamp() as time_t);
}
}
_ => continue,
}
}
None
});
// 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 = 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)?
}
/// The format used to display a FormattedUptime.
pub enum OutputFormat {
/// Typical `uptime` output (e.g. 2 days, 3:04).
HumanReadable,
/// Pretty printed output (e.g. 2 days, 3 hours, 04 minutes).
PrettyPrint,
}
struct FormattedUptime {
up_days: i64,
up_hours: i64,
up_mins: i64,
}
impl FormattedUptime {
fn new(up_secs: i64) -> Self {
let up_days = up_secs / 86400;
let up_hours = (up_secs - (up_days * 86400)) / 3600;
let up_mins = (up_secs - (up_days * 86400) - (up_hours * 3600)) / 60;
Self {
up_days,
up_hours,
up_mins,
}
}
fn get_human_readable_uptime(&self) -> String {
translate!(
"uptime-format",
"days" => self.up_days,
"time" => format!("{:02}:{:02}", self.up_hours, self.up_mins))
}
fn get_pretty_print_uptime(&self) -> String {
let mut parts = Vec::new();
if self.up_days > 0 {
parts.push(translate!("uptime-format-pretty-day", "day" => self.up_days));
}
if self.up_hours > 0 {
parts.push(translate!("uptime-format-pretty-hour", "hour" => self.up_hours));
}
if self.up_mins > 0 || parts.is_empty() {
parts.push(translate!("uptime-format-pretty-min", "min" => self.up_mins));
}
parts.join(", ")
}
}
/// Get the system uptime
///
/// # Arguments
///
/// boot_time will be ignored, pass None.
///
/// # Returns
///
/// Returns a UResult with the uptime in seconds if successful, otherwise an UptimeError.
#[cfg(windows)]
pub fn get_uptime(_boot_time: Option<time_t>) -> UResult<i64> {
ensure_uptime_locale();
use windows_sys::Win32::System::SystemInformation::GetTickCount;
// SAFETY: always return u32
let uptime = unsafe { GetTickCount() };
Ok(uptime as i64 / 1000)
}
/// Get the system uptime in a human-readable format
///
/// # Arguments
///
/// boot_time: Option<time_t> - Manually specify the boot time, or None to try to get it from the system.
/// output_format: OutputFormat - Selects the format of the output string.
///
/// # Returns
///
/// Returns a UResult with the uptime in a human-readable format(e.g. "1 day, 3:45") if successful, otherwise an UptimeError.
#[inline]
pub fn get_formatted_uptime(
boot_time: Option<time_t>,
output_format: OutputFormat,
) -> UResult<String> {
ensure_uptime_locale();
let up_secs = get_uptime(boot_time)?;
if up_secs < 0 {
Err(UptimeError::SystemUptime)?;
}
let formatted_uptime = FormattedUptime::new(up_secs);
match output_format {
OutputFormat::HumanReadable => Ok(formatted_uptime.get_human_readable_uptime()),
OutputFormat::PrettyPrint => Ok(formatted_uptime.get_pretty_print_uptime()),
}
}
/// Get the number of users currently logged in
///
/// # Returns
///
/// Returns the number of users currently logged in if successful, otherwise 0.
#[cfg(unix)]
#[cfg(not(target_os = "openbsd"))]
// see: https://gitlab.com/procps-ng/procps/-/blob/4740a0efa79cade867cfc7b32955fe0f75bf5173/library/uptime.c#L63-L115
pub fn get_nusers() -> usize {
use crate::utmpx::Utmpx;
use libc::USER_PROCESS;
let mut num_user = 0;
Utmpx::iter_all_records().for_each(|ut| {
if ut.record_type() == USER_PROCESS {
num_user += 1;
}
});
num_user
}
/// Get the number of users currently logged in
///
/// # Returns
///
/// Returns the number of users currently logged in if successful, otherwise 0
#[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;
}
// Count entries that have a non-empty user field
entries
.iter()
.filter_map(|entry| match entry {
UtmpEntry::UTMP { user, .. } if !user.is_empty() => Some(()),
_ => None,
})
.count()
}
/// Get the number of users currently logged in
///
/// # Returns
///
/// Returns the number of users currently logged in if successful, otherwise 0
#[cfg(target_os = "windows")]
pub fn get_nusers() -> usize {
use std::ptr;
use windows_sys::Win32::System::RemoteDesktop::*;
let mut num_user = 0;
// SAFETY: WTS_CURRENT_SERVER_HANDLE is a valid handle
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
}
/// Format the number of users to a human-readable string
///
/// # Returns
///
/// e.g. "0 users", "1 user", "2 users"
#[inline]
pub fn format_nusers(n: usize) -> String {
ensure_uptime_locale();
translate!(
"uptime-user-count",
"count" => n
)
}
/// Get the number of users currently logged in in a human-readable format
///
/// # Returns
///
/// e.g. "0 user", "1 user", "2 users"
#[inline]
pub fn get_formatted_nusers() -> String {
ensure_uptime_locale();
#[cfg(not(target_os = "openbsd"))]
return format_nusers(get_nusers());
#[cfg(target_os = "openbsd")]
format_nusers(get_nusers("/var/run/utmp"))
}
/// Get the system load average
///
/// # Returns
///
/// Returns a UResult with the load average if successful, otherwise an UptimeError.
/// The load average is a tuple of three floating point numbers representing the 1-minute, 5-minute, and 15-minute load averages.
#[cfg(unix)]
pub fn get_loadavg() -> UResult<(f64, f64, f64)> {
ensure_uptime_locale();
use crate::libc::c_double;
use libc::getloadavg;
let mut avg: [c_double; 3] = [0.0; 3];
// SAFETY: checked whether it returns -1
let loads: i32 = unsafe { getloadavg(avg.as_mut_ptr(), 3) };
if loads == -1 {
Err(UptimeError::SystemLoadavg)?
} else {
Ok((avg[0], avg[1], avg[2]))
}
}
/// Get the system load average
/// Windows does not have an equivalent to the load average on Unix-like systems.
///
/// # Returns
///
/// Returns a UResult with an UptimeError.
#[cfg(windows)]
pub fn get_loadavg() -> UResult<(f64, f64, f64)> {
ensure_uptime_locale();
Err(UptimeError::WindowsLoadavg)?
}
/// Get the system load average in a human-readable format
///
/// # Returns
///
/// Returns a UResult with the load average in a human-readable format if successful, otherwise an UptimeError.
/// e.g. "load average: 0.00, 0.00, 0.00"
#[inline]
pub fn get_formatted_loadavg() -> UResult<String> {
ensure_uptime_locale();
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]
fn test_format_nusers_threaded() {
unsafe {
std::env::set_var("LANG", "en_US.UTF-8");
}
let _ = locale::setup_localization("top");
let _ = locale::setup_localization("uptime");
assert_eq!("uptime-user-count", format_nusers(0));
std::thread::spawn(move || {
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));
})
.join()
.expect("thread should succeed");
}
/// 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 = Timestamp::now().as_second();
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: {uptime} seconds"
);
}
/// 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 {uptime1} and {uptime2}"
);
}
}