1
use anyhow::Context;
2
use clap::{CommandFactory, Parser};
3
use clap_complete::{Shell, generate};
4
use roowho2_lib::{
5
    proto::WhodUserEntry,
6
    server::varlink_api::{VarlinkRwhodClientError, VarlinkRwhodClientProxy},
7
};
8

            
9
/// Check who is logged in on local machines.
10
///
11
/// The `rwho` command produces output similar to `who`, but for all machines on the local network.
12
/// If no report has been received from a machine for 11 minutes then rwho assumes the machine is down,
13
/// and does not report users last known to be logged into that machine.
14
///
15
/// If a users hasn't typed to the system for a minute or more, then rwho reports this idle time.
16
/// If a user hasn't typed to the system for an hour or more,
17
/// then the user will be omitted from the output of `rwho` unless the `-a` flag is given.
18
#[derive(Debug, Parser)]
19
#[command(author = "Programvareverkstedet <projects@pvv.ntnu.no>", version)]
20
pub struct Args {
21
    /// Print all machines responding even if no one is currently logged in
22
    #[arg(long, short)]
23
    all: bool,
24

            
25
    /// Print the output with the old formatting
26
    #[arg(long, short)]
27
    old: bool,
28

            
29
    /// Output in JSON format
30
    #[arg(long, short)]
31
    json: bool,
32

            
33
    /// Generate shell completion scripts for the specified shell
34
    /// and print them to stdout.
35
    #[arg(long, value_enum, hide = true)]
36
    completions: Option<Shell>,
37
}
38

            
39
#[tokio::main]
40
async fn main() -> anyhow::Result<()> {
41
    let args = Args::parse();
42

            
43
    if let Some(shell) = args.completions {
44
        generate(shell, &mut Args::command(), "rwho", &mut std::io::stdout());
45
        return Ok(());
46
    }
47

            
48
    let mut conn = zlink::unix::connect("/run/roowho2/roowho2.varlink")
49
        .await
50
        .expect("Failed to connect to rwhod server");
51

            
52
    let mut reply = conn
53
        .rwho(args.all)
54
        .await
55
        .context("Failed to send rwho request")?
56
        .map_err(|e| match e {
57
            VarlinkRwhodClientError::Disabled => {
58
                anyhow::anyhow!("The rwhod service is disabled on the server")
59
            }
60
            VarlinkRwhodClientError::TimedOut => {
61
                anyhow::anyhow!("The rwhod service timed out while processing the request")
62
            }
63
            VarlinkRwhodClientError::InvalidRequest => {
64
                anyhow::anyhow!("The rwhod service could not process the request, please check the logs or report the error to your system administrators")
65
            }
66
        })?;
67

            
68
    reply.sort_by(|(host, user), (host2, user2)| {
69
        user.user_id
70
            .cmp(&user2.user_id)
71
            .then_with(|| host.cmp(host2))
72
            .then_with(|| user.tty.cmp(&user2.tty))
73
    });
74

            
75
    if args.json {
76
        println!("{}", serde_json::to_string_pretty(&reply).unwrap());
77
    } else if args.old {
78
        old_format_user_entries(&reply)
79
            .iter()
80
            .for_each(|line| println!("{}", line));
81
    } else {
82
        old_format_user_entries(&reply)
83
            .iter()
84
            .for_each(|line| println!("{}", line));
85
    }
86

            
87
    Ok(())
88
}
89

            
90
fn old_format_user_entries(entries: &[(String, WhodUserEntry)]) -> Vec<String> {
91
    let hostname_tty_width = entries
92
        .iter()
93
        .map(|(host, user)| host.len() + user.tty.len() + 1)
94
        .max()
95
        .unwrap_or(0);
96

            
97
    let idle_time_width = entries
98
        .iter()
99
        .map(|(_, user)| user.idle_time.num_hours())
100
        .max()
101
        .map(|hours| {
102
            if hours >= 10 {
103
                5
104
            } else if hours > 0 {
105
                4
106
            } else {
107
                3
108
            }
109
        })
110
        .unwrap_or(0);
111

            
112
    entries
113
        .iter()
114
        .map(|(hostname, user)| {
115
            old_format_user_entry(hostname, hostname_tty_width, idle_time_width, user)
116
        })
117
        .collect()
118
}
119

            
120
fn old_format_user_entry(
121
    hostname: &str,
122
    hostname_tty_width: usize,
123
    idle_time_width: usize,
124
    user: &WhodUserEntry,
125
) -> String {
126
    let idle_str = {
127
        let hours = user.idle_time.num_hours().min(99);
128
        let minutes = user.idle_time.num_minutes() % 60;
129
        format!(
130
            "{}:{:02}",
131
            if hours == 0 {
132
                "".to_string()
133
            } else {
134
                hours.to_string()
135
            },
136
            minutes
137
        )
138
    };
139

            
140
    format!(
141
        "{:<8.8} {:<hostname_tty_width$} {:.12} {:>idle_time_width$}",
142
        user.user_id,
143
        format!("{hostname}:{}", user.tty),
144
        user.login_time.format("%b %d %H:%M"),
145
        idle_str,
146
    )
147
}