Skip to main content

uucore/features/
utmpx.rs

1// This file is part of the uutils coreutils package.
2//
3// For the full copyright and license information, please view the LICENSE
4// file that was distributed with this source code.
5//
6// spell-checker:ignore IDLEN logind
7
8//! Aims to provide platform-independent methods to obtain login records
9//!
10//! **ONLY** support linux, macos and freebsd for the time being
11//!
12//! # Examples:
13//!
14//! ```
15//! use uucore::utmpx::Utmpx;
16//! for ut in Utmpx::iter_all_records() {
17//!     if ut.is_user_process() {
18//!         println!("{}: {}", ut.host(), ut.user())
19//!     }
20//! }
21//! ```
22//!
23//! Specifying the path to login record:
24//!
25//! ```
26//! use uucore::utmpx::Utmpx;
27//! for ut in Utmpx::iter_all_records_from("/some/where/else") {
28//!     if ut.is_user_process() {
29//!         println!("{}: {}", ut.host(), ut.user())
30//!     }
31//! }
32//! ```
33
34pub extern crate time;
35
36use std::ffi::CString;
37use std::io::Result as IOResult;
38use std::marker::PhantomData;
39use std::os::unix::ffi::OsStrExt;
40use std::path::Path;
41use std::ptr;
42use std::sync::{Mutex, MutexGuard};
43
44#[cfg(feature = "feat_systemd_logind")]
45use crate::features::systemd_logind;
46
47pub use self::ut::*;
48
49// See the FAQ at https://wiki.musl-libc.org/faq#Q:-Why-is-the-utmp/wtmp-functionality-only-implemented-as-stubs?
50// Musl implements only stubs for the utmp functions, and the libc crate issues a deprecation warning about this.
51// However, calling these stubs is the correct approach to maintain consistent behavior with GNU coreutils.
52#[cfg_attr(target_env = "musl", allow(deprecated))]
53pub use libc::endutxent;
54#[cfg_attr(target_env = "musl", allow(deprecated))]
55pub use libc::getutxent;
56#[cfg_attr(target_env = "musl", allow(deprecated))]
57pub use libc::setutxent;
58use libc::utmpx;
59#[cfg(any(
60    target_vendor = "apple",
61    target_os = "linux",
62    target_os = "netbsd",
63    target_os = "cygwin"
64))]
65#[cfg_attr(target_env = "musl", allow(deprecated))]
66pub use libc::utmpxname;
67
68/// # Safety
69/// Just fixed the clippy warning. Please add description here.
70#[cfg(target_os = "freebsd")]
71pub unsafe extern "C" fn utmpxname(_file: *const libc::c_char) -> libc::c_int {
72    0
73}
74
75use crate::libc; // import macros from `../../macros.rs`
76
77// In case the c_char array doesn't end with NULL
78macro_rules! chars2string {
79    ($arr:expr) => {
80        $arr.iter()
81            .take_while(|i| **i > 0)
82            .map(|&i| i as u8 as char)
83            .collect::<String>()
84    };
85}
86
87#[cfg(target_os = "linux")]
88mod ut {
89    pub static DEFAULT_FILE: &str = "/var/run/utmp";
90
91    #[cfg(not(target_env = "musl"))]
92    pub use libc::__UT_HOSTSIZE as UT_HOSTSIZE;
93    #[cfg(target_env = "musl")]
94    pub use libc::UT_HOSTSIZE;
95
96    #[cfg(not(target_env = "musl"))]
97    pub use libc::__UT_LINESIZE as UT_LINESIZE;
98    #[cfg(target_env = "musl")]
99    pub use libc::UT_LINESIZE;
100
101    #[cfg(not(target_env = "musl"))]
102    pub use libc::__UT_NAMESIZE as UT_NAMESIZE;
103    #[cfg(target_env = "musl")]
104    pub use libc::UT_NAMESIZE;
105
106    pub const UT_IDSIZE: usize = 4;
107
108    pub use libc::ACCOUNTING;
109    pub use libc::BOOT_TIME;
110    pub use libc::DEAD_PROCESS;
111    pub use libc::EMPTY;
112    pub use libc::INIT_PROCESS;
113    pub use libc::LOGIN_PROCESS;
114    pub use libc::NEW_TIME;
115    pub use libc::OLD_TIME;
116    pub use libc::RUN_LVL;
117    pub use libc::USER_PROCESS;
118}
119
120#[cfg(target_vendor = "apple")]
121mod ut {
122    pub static DEFAULT_FILE: &str = "/var/run/utmpx";
123
124    pub use libc::_UTX_HOSTSIZE as UT_HOSTSIZE;
125    pub use libc::_UTX_IDSIZE as UT_IDSIZE;
126    pub use libc::_UTX_LINESIZE as UT_LINESIZE;
127    pub use libc::_UTX_USERSIZE as UT_NAMESIZE;
128
129    pub use libc::ACCOUNTING;
130    pub use libc::BOOT_TIME;
131    pub use libc::DEAD_PROCESS;
132    pub use libc::EMPTY;
133    pub use libc::INIT_PROCESS;
134    pub use libc::LOGIN_PROCESS;
135    pub use libc::NEW_TIME;
136    pub use libc::OLD_TIME;
137    pub use libc::RUN_LVL;
138    pub use libc::SHUTDOWN_TIME;
139    pub use libc::SIGNATURE;
140    pub use libc::USER_PROCESS;
141}
142
143#[cfg(target_os = "freebsd")]
144mod ut {
145    pub static DEFAULT_FILE: &str = "";
146
147    pub const UT_LINESIZE: usize = 16;
148    pub const UT_NAMESIZE: usize = 32;
149    pub const UT_IDSIZE: usize = 8;
150    pub const UT_HOSTSIZE: usize = 128;
151
152    pub use libc::BOOT_TIME;
153    pub use libc::DEAD_PROCESS;
154    pub use libc::EMPTY;
155    pub use libc::INIT_PROCESS;
156    pub use libc::LOGIN_PROCESS;
157    pub use libc::NEW_TIME;
158    pub use libc::OLD_TIME;
159    pub use libc::SHUTDOWN_TIME;
160    pub use libc::USER_PROCESS;
161}
162
163#[cfg(target_os = "netbsd")]
164mod ut {
165    pub static DEFAULT_FILE: &str = "/var/run/utmpx";
166
167    pub const SHUTDOWN_TIME: usize = 11;
168
169    pub use libc::_UTX_HOSTSIZE as UT_HOSTSIZE;
170    pub use libc::_UTX_IDSIZE as UT_IDSIZE;
171    pub use libc::_UTX_LINESIZE as UT_LINESIZE;
172    pub use libc::_UTX_USERSIZE as UT_NAMESIZE;
173
174    pub use libc::ACCOUNTING;
175    pub const BOOT_TIME: i16 = libc::BOOT_TIME as i16;
176    pub const DEAD_PROCESS: i16 = libc::DEAD_PROCESS as i16;
177    pub const EMPTY: i16 = libc::EMPTY as i16;
178    pub const INIT_PROCESS: i16 = libc::INIT_PROCESS as i16;
179    pub const LOGIN_PROCESS: i16 = libc::LOGIN_PROCESS as i16;
180    pub const NEW_TIME: i16 = libc::NEW_TIME as i16;
181    pub const OLD_TIME: i16 = libc::OLD_TIME as i16;
182    pub const RUN_LVL: i16 = libc::RUN_LVL as i16;
183    pub const SIGNATURE: i16 = libc::SIGNATURE as i16;
184    pub const USER_PROCESS: i16 = libc::USER_PROCESS as i16;
185}
186
187#[cfg(target_os = "cygwin")]
188mod ut {
189    pub static DEFAULT_FILE: &str = "";
190
191    pub use libc::UT_HOSTSIZE;
192    pub use libc::UT_IDLEN;
193    pub use libc::UT_LINESIZE;
194    pub use libc::UT_NAMESIZE;
195
196    pub use libc::BOOT_TIME;
197    pub use libc::DEAD_PROCESS;
198    pub use libc::INIT_PROCESS;
199    pub use libc::LOGIN_PROCESS;
200    pub use libc::NEW_TIME;
201    pub use libc::OLD_TIME;
202    pub use libc::RUN_LVL;
203    pub use libc::USER_PROCESS;
204}
205
206/// A login record
207pub struct Utmpx {
208    inner: utmpx,
209}
210
211#[cfg(target_os = "netbsd")]
212impl Utmpx {
213    fn ut_type(&self) -> i16 {
214        self.inner.ut_type as i16
215    }
216    fn ut_user(&self) -> String {
217        chars2string!(self.inner.ut_name)
218    }
219}
220
221#[cfg(not(target_os = "netbsd"))]
222impl Utmpx {
223    fn ut_type(&self) -> i16 {
224        self.inner.ut_type
225    }
226    fn ut_user(&self) -> String {
227        chars2string!(self.inner.ut_user)
228    }
229}
230
231impl Utmpx {
232    /// A.K.A. ut.ut_type
233    pub fn record_type(&self) -> i16 {
234        self.ut_type()
235    }
236    /// A.K.A. ut.ut_pid
237    pub fn pid(&self) -> i32 {
238        self.inner.ut_pid
239    }
240    /// A.K.A. ut.ut_id
241    pub fn terminal_suffix(&self) -> String {
242        chars2string!(self.inner.ut_id)
243    }
244    ///  A.K.A. ut.ut_user / ut.ut_name (NetBSD)
245    pub fn user(&self) -> String {
246        self.ut_user()
247    }
248    /// A.K.A. ut.ut_host
249    pub fn host(&self) -> String {
250        chars2string!(self.inner.ut_host)
251    }
252    /// A.K.A. ut.ut_line
253    pub fn tty_device(&self) -> String {
254        chars2string!(self.inner.ut_line)
255    }
256    /// A.K.A. ut.ut_tv
257    pub fn login_time(&self) -> time::OffsetDateTime {
258        #[allow(clippy::unnecessary_cast)]
259        let ts_nanos: i128 = (1_000_000_000_i64 * self.inner.ut_tv.tv_sec as i64
260            + 1_000_i64 * self.inner.ut_tv.tv_usec as i64)
261            .into();
262        let local_offset = time::OffsetDateTime::now_local()
263            .map_or_else(|_| time::UtcOffset::UTC, time::OffsetDateTime::offset);
264        time::OffsetDateTime::from_unix_timestamp_nanos(ts_nanos)
265            .unwrap()
266            .to_offset(local_offset)
267    }
268    /// A.K.A. ut.ut_exit
269    ///
270    /// Return (e_termination, e_exit)
271    #[cfg(target_os = "linux")]
272    pub fn exit_status(&self) -> (i16, i16) {
273        (self.inner.ut_exit.e_termination, self.inner.ut_exit.e_exit)
274    }
275    /// A.K.A. ut.ut_exit
276    ///
277    /// Return (0, 0) on Non-Linux platform
278    #[cfg(not(target_os = "linux"))]
279    pub fn exit_status(&self) -> (i16, i16) {
280        (0, 0)
281    }
282    /// Consumes the `Utmpx`, returning the underlying C struct utmpx
283    pub fn into_inner(self) -> utmpx {
284        self.inner
285    }
286    /// check if the record is a user process
287    pub fn is_user_process(&self) -> bool {
288        !self.user().is_empty() && self.record_type() == USER_PROCESS
289    }
290
291    /// Canonicalize host name using DNS
292    pub fn canon_host(&self) -> IOResult<String> {
293        let host = self.host();
294
295        let (hostname, display) = host.split_once(':').unwrap_or((&host, ""));
296
297        if !hostname.is_empty() {
298            use dns_lookup::{AddrInfoHints, getaddrinfo};
299
300            const AI_CANONNAME: i32 = 0x2;
301            let hints = AddrInfoHints {
302                flags: AI_CANONNAME,
303                ..AddrInfoHints::default()
304            };
305            if let Ok(sockets) = getaddrinfo(Some(hostname), None, Some(hints)) {
306                let sockets = sockets.collect::<IOResult<Vec<_>>>()?;
307                for socket in sockets {
308                    if let Some(ai_canonname) = socket.canonname {
309                        return Ok(if display.is_empty() {
310                            ai_canonname
311                        } else {
312                            format!("{ai_canonname}:{display}")
313                        });
314                    }
315                }
316            } else {
317                // GNU coreutils has this behavior
318                return Ok(hostname.to_string());
319            }
320        }
321
322        Ok(host)
323    }
324
325    /// Iterate through all the utmp records.
326    ///
327    /// This will use the default location, or the path [`Utmpx::iter_all_records_from`]
328    /// was most recently called with.
329    ///
330    /// On systems with systemd-logind feature enabled at compile time,
331    /// this will use systemd-logind instead of traditional utmp files.
332    ///
333    /// Only one instance of [`UtmpxIter`] may be active at a time. This
334    /// function will block as long as one is still active. Beware!
335    pub fn iter_all_records() -> UtmpxIter {
336        #[cfg(feature = "feat_systemd_logind")]
337        {
338            // Use systemd-logind instead of traditional utmp when feature is enabled
339            UtmpxIter::new_systemd()
340        }
341
342        #[cfg(not(feature = "feat_systemd_logind"))]
343        {
344            let iter = UtmpxIter::new();
345            unsafe {
346                // This can technically fail, and it would be nice to detect that,
347                // but it doesn't return anything so we'd have to do nasty things
348                // with errno.
349                #[cfg_attr(target_env = "musl", allow(deprecated))]
350                setutxent();
351            }
352            iter
353        }
354    }
355
356    /// Iterate through all the utmp records from a specific file.
357    ///
358    /// No failure is reported or detected.
359    ///
360    /// This function affects subsequent calls to [`Utmpx::iter_all_records`].
361    ///
362    /// On systems with systemd-logind feature enabled at compile time,
363    /// if the path matches the default utmp file, this will use systemd-logind
364    /// instead of traditional utmp files.
365    ///
366    /// The same caveats as for [`Utmpx::iter_all_records`] apply.
367    pub fn iter_all_records_from<P: AsRef<Path>>(path: P) -> UtmpxIter {
368        #[cfg(feature = "feat_systemd_logind")]
369        {
370            // Use systemd-logind for default utmp file when feature is enabled
371            if path.as_ref() == Path::new(DEFAULT_FILE) {
372                return UtmpxIter::new_systemd();
373            }
374        }
375
376        let iter = UtmpxIter::new();
377        let path = CString::new(path.as_ref().as_os_str().as_bytes()).unwrap();
378        unsafe {
379            // In glibc, utmpxname() only fails if there's not enough memory
380            // to copy the string.
381            // Solaris returns 1 on success instead of 0. Supposedly there also
382            // exist systems where it returns void.
383            // GNU who on Debian seems to output nothing if an invalid filename
384            // is specified, no warning or anything.
385            // So this function is pretty crazy and we don't try to detect errors.
386            // Not much we can do besides pray.
387            #[cfg_attr(target_env = "musl", allow(deprecated))]
388            utmpxname(path.as_ptr());
389            #[cfg_attr(target_env = "musl", allow(deprecated))]
390            setutxent();
391        }
392        iter
393    }
394}
395
396// On some systems these functions are not thread-safe. On others they're
397// thread-local. Therefore we use a mutex to allow only one guard to exist at
398// a time, and make sure UtmpxIter cannot be sent across threads.
399//
400// I believe the only technical memory unsafety that could happen is a data
401// race while copying the data out of the pointer returned by getutxent(), but
402// ordinary race conditions are also very much possible.
403static LOCK: Mutex<()> = Mutex::new(());
404
405/// Iterator of login records
406pub struct UtmpxIter {
407    #[allow(dead_code)]
408    guard: MutexGuard<'static, ()>,
409    /// Ensure UtmpxIter is !Send. Technically redundant because MutexGuard
410    /// is also !Send.
411    phantom: PhantomData<std::rc::Rc<()>>,
412    #[cfg(feature = "feat_systemd_logind")]
413    systemd_iter: Option<systemd_logind::SystemdUtmpxIter>,
414}
415
416impl UtmpxIter {
417    fn new() -> Self {
418        // PoisonErrors can safely be ignored
419        let guard = LOCK
420            .lock()
421            .unwrap_or_else(std::sync::PoisonError::into_inner);
422        Self {
423            guard,
424            phantom: PhantomData,
425            #[cfg(feature = "feat_systemd_logind")]
426            systemd_iter: None,
427        }
428    }
429
430    #[cfg(feature = "feat_systemd_logind")]
431    fn new_systemd() -> Self {
432        // PoisonErrors can safely be ignored
433        let guard = LOCK
434            .lock()
435            .unwrap_or_else(std::sync::PoisonError::into_inner);
436        let systemd_iter = match systemd_logind::SystemdUtmpxIter::new() {
437            Ok(iter) => iter,
438            Err(_) => {
439                // Like GNU coreutils: graceful degradation, not fallback to traditional utmp
440                // Return empty iterator rather than falling back  (GNU coreutils also returns 0 when /var/run/utmp is not present, so we don't need to propagate the error here)
441                systemd_logind::SystemdUtmpxIter::empty()
442            }
443        };
444        Self {
445            guard,
446            phantom: PhantomData,
447            systemd_iter: Some(systemd_iter),
448        }
449    }
450}
451
452/// Wrapper type that can hold either traditional utmpx records or systemd records
453pub enum UtmpxRecord {
454    Traditional(Box<Utmpx>),
455    #[cfg(feature = "feat_systemd_logind")]
456    Systemd(systemd_logind::SystemdUtmpxCompat),
457}
458
459impl UtmpxRecord {
460    /// A.K.A. ut.ut_type
461    pub fn record_type(&self) -> i16 {
462        match self {
463            Self::Traditional(utmpx) => utmpx.record_type(),
464            #[cfg(feature = "feat_systemd_logind")]
465            Self::Systemd(systemd) => systemd.record_type(),
466        }
467    }
468
469    /// A.K.A. ut.ut_pid
470    pub fn pid(&self) -> i32 {
471        match self {
472            Self::Traditional(utmpx) => utmpx.pid(),
473            #[cfg(feature = "feat_systemd_logind")]
474            Self::Systemd(systemd) => systemd.pid(),
475        }
476    }
477
478    /// A.K.A. ut.ut_id
479    pub fn terminal_suffix(&self) -> String {
480        match self {
481            Self::Traditional(utmpx) => utmpx.terminal_suffix(),
482            #[cfg(feature = "feat_systemd_logind")]
483            Self::Systemd(systemd) => systemd.terminal_suffix(),
484        }
485    }
486
487    /// A.K.A. ut.ut_user
488    pub fn user(&self) -> String {
489        match self {
490            Self::Traditional(utmpx) => utmpx.user(),
491            #[cfg(feature = "feat_systemd_logind")]
492            Self::Systemd(systemd) => systemd.user(),
493        }
494    }
495
496    /// A.K.A. ut.ut_host
497    pub fn host(&self) -> String {
498        match self {
499            Self::Traditional(utmpx) => utmpx.host(),
500            #[cfg(feature = "feat_systemd_logind")]
501            Self::Systemd(systemd) => systemd.host(),
502        }
503    }
504
505    /// A.K.A. ut.ut_line
506    pub fn tty_device(&self) -> String {
507        match self {
508            Self::Traditional(utmpx) => utmpx.tty_device(),
509            #[cfg(feature = "feat_systemd_logind")]
510            Self::Systemd(systemd) => systemd.tty_device(),
511        }
512    }
513
514    /// A.K.A. ut.ut_tv
515    pub fn login_time(&self) -> time::OffsetDateTime {
516        match self {
517            Self::Traditional(utmpx) => utmpx.login_time(),
518            #[cfg(feature = "feat_systemd_logind")]
519            Self::Systemd(systemd) => systemd.login_time(),
520        }
521    }
522
523    /// A.K.A. ut.ut_exit
524    ///
525    /// Return (e_termination, e_exit)
526    pub fn exit_status(&self) -> (i16, i16) {
527        match self {
528            Self::Traditional(utmpx) => utmpx.exit_status(),
529            #[cfg(feature = "feat_systemd_logind")]
530            Self::Systemd(systemd) => systemd.exit_status(),
531        }
532    }
533
534    /// check if the record is a user process
535    pub fn is_user_process(&self) -> bool {
536        match self {
537            Self::Traditional(utmpx) => utmpx.is_user_process(),
538            #[cfg(feature = "feat_systemd_logind")]
539            Self::Systemd(systemd) => systemd.is_user_process(),
540        }
541    }
542
543    /// Canonicalize host name using DNS
544    pub fn canon_host(&self) -> IOResult<String> {
545        match self {
546            Self::Traditional(utmpx) => utmpx.canon_host(),
547            #[cfg(feature = "feat_systemd_logind")]
548            Self::Systemd(systemd) => Ok(systemd.canon_host()),
549        }
550    }
551}
552
553impl Iterator for UtmpxIter {
554    type Item = UtmpxRecord;
555    fn next(&mut self) -> Option<Self::Item> {
556        #[cfg(feature = "feat_systemd_logind")]
557        {
558            if let Some(ref mut systemd_iter) = self.systemd_iter {
559                // We have a systemd iterator - use it exclusively (never fall back to traditional utmp)
560                return systemd_iter.next().map(UtmpxRecord::Systemd);
561            }
562        }
563
564        // Traditional utmp path
565        unsafe {
566            #[cfg_attr(target_env = "musl", allow(deprecated))]
567            let res = getutxent();
568            if res.is_null() {
569                None
570            } else {
571                // The data behind this pointer will be replaced by the next
572                // call to getutxent(), so we have to read it now.
573                // All the strings live inline in the struct as arrays, which
574                // makes things easier.
575                Some(UtmpxRecord::Traditional(Box::new(Utmpx {
576                    inner: ptr::read(res.cast_const()),
577                })))
578            }
579        }
580    }
581}
582
583impl Drop for UtmpxIter {
584    fn drop(&mut self) {
585        unsafe {
586            #[cfg_attr(target_env = "musl", allow(deprecated))]
587            endutxent();
588        }
589    }
590}