last_rs/
lib.rs

1use thiserror::Error;
2use time::OffsetDateTime;
3use utmp_rs::UtmpEntry;
4
5// An exit event (logout, system crash, powered off)
6#[derive(Debug)]
7pub enum Exit {
8    Logout(OffsetDateTime),
9    Crash(OffsetDateTime),
10    Reboot(OffsetDateTime),
11    StillLoggedIn,
12}
13
14// An enter event (login, system boot, etc.)
15#[derive(Debug)]
16pub struct Enter {
17    pub user: String,
18    pub host: String,
19    pub line: String,
20    pub login_time: OffsetDateTime,
21    pub exit: Exit,
22}
23
24#[derive(Error, Debug)]
25pub enum LastError {
26    #[error(transparent)]
27    UtmpParse(#[from] utmp_rs::ParseError),
28}
29
30// We found a login
31// Now iterate through the next enrties to find the accomanying logout
32// It will be the next DEAD_PROCESS with the same ut_line
33// Source: `last` source code
34fn find_accompanying_logout(entries: &[UtmpEntry], target_line: &str) -> Option<Exit> {
35    entries.iter().rev().find_map(|x| match x {
36        // If we see a DEAD_PROCESS with the same line as the login, then it's a logout event
37        UtmpEntry::DeadProcess { line, time, .. } if line == target_line => {
38            Some(Exit::Logout(*time))
39        }
40        UtmpEntry::ShutdownTime { time, .. } => Some(Exit::Reboot(*time)),
41        UtmpEntry::BootTime { time, .. } => Some(Exit::Crash(*time)),
42        _ => None,
43    })
44}
45
46pub fn get_logins(file: &str) -> Result<Vec<Enter>, LastError> {
47    // let entries = utmp_rs::parse_from_path("/var/run/utmp")?;
48    let mut entries = utmp_rs::parse_from_path(file)?;
49    entries.reverse();
50    Ok(entries
51        .iter()
52        .enumerate()
53        .filter_map(|(i, x)| match x {
54            UtmpEntry::UserProcess {
55                user,
56                host,
57                time,
58                line,
59                ..
60            } => {
61                let exit = find_accompanying_logout(&entries[..i], &line[..])
62                    .unwrap_or(Exit::StillLoggedIn);
63                Some(Enter {
64                    user: user.to_owned(),
65                    host: host.to_owned(),
66                    line: line.to_owned(),
67                    login_time: *time,
68                    exit,
69                })
70            }
71            _ => None,
72        })
73        .collect())
74}