1use thiserror::Error;
2use time::OffsetDateTime;
3use utmp_rs::UtmpEntry;
4
5#[derive(Debug)]
7pub enum Exit {
8 Logout(OffsetDateTime),
9 Crash(OffsetDateTime),
10 Reboot(OffsetDateTime),
11 StillLoggedIn,
12}
13
14#[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
30fn find_accompanying_logout(entries: &[UtmpEntry], target_line: &str) -> Option<Exit> {
35 entries.iter().rev().find_map(|x| match x {
36 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 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}