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

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

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

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

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

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

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

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

            
53
    let mut reply = conn
54
        .rwho(args.all)
55
        .await
56
        .context("Failed to send rwho request")?
57
        .map_err(|e| anyhow::anyhow!("Server returned an error for rwho request: {:?}", e))?;
58

            
59
    if !args.all {
60
        reply.retain(|(_, user)| user.idle_time.num_minutes() <= 11);
61
    }
62

            
63
    reply.sort_by(|(host, user), (host2, user2)| {
64
        user.user_id
65
            .cmp(&user2.user_id)
66
            .then_with(|| host.cmp(host2))
67
            .then_with(|| user.tty.cmp(&user2.tty))
68
    });
69

            
70
    if args.json {
71
        println!("{}", serde_json::to_string_pretty(&reply).unwrap());
72
    } else if args.old {
73
        old_format_user_entries(&reply)
74
            .iter()
75
            .for_each(|line| println!("{}", line));
76
    } else {
77
        old_format_user_entries(&reply)
78
            .iter()
79
            .for_each(|line| println!("{}", line));
80
    }
81

            
82
    Ok(())
83
}
84

            
85
fn old_format_user_entries(entries: &[(String, WhodUserEntry)]) -> Vec<String> {
86
    let hostname_tty_width = entries
87
        .iter()
88
        .map(|(host, user)| host.len() + user.tty.len() + 1)
89
        .max()
90
        .unwrap_or(0);
91

            
92
    let idle_time_width = entries
93
        .iter()
94
        .map(|(_, user)| user.idle_time.num_hours())
95
        .max()
96
        .map(|hours| {
97
            if hours >= 10 {
98
                5
99
            } else if hours > 0 {
100
                4
101
            } else {
102
                3
103
            }
104
        })
105
        .unwrap_or(0);
106

            
107
    entries
108
        .iter()
109
        .map(|(hostname, user)| {
110
            old_format_user_entry(hostname, hostname_tty_width, idle_time_width, user)
111
        })
112
        .collect()
113
}
114

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

            
135
    format!(
136
        "{:<8.8} {:<hostname_tty_width$} {:.12} {:>idle_time_width$}",
137
        user.user_id,
138
        format!("{hostname}:{}", user.tty),
139
        user.login_time.format("%b %d %H:%M"),
140
        idle_str,
141
    )
142
}