1
use std::collections::BTreeMap;
2

            
3
use anyhow::Context;
4
use clap::{CommandFactory, Parser};
5
use clap_complete::{Shell, generate};
6
use roowho2_lib::{
7
    proto::WhodUserEntry,
8
    server::varlink_api::{
9
        DEFAULT_CLIENT_SERVER_SOCKET_PATH, VarlinkRwhodClientError, VarlinkRwhodClientProxy,
10
    },
11
    version,
12
};
13

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

            
34
    /// Only show users idle for less than this many minutes.
35
    /// Cannot be used together with `--all`.
36
    #[arg(long, value_name = "MINUTES", default_value_t = DEFAULT_IDLE_TIMEOUT_MINUTES)]
37
    idle_timeout: u32,
38

            
39
    /// Print the output with the old formatting
40
    #[arg(long, short)]
41
    old: bool,
42

            
43
    /// Output in JSON format
44
    #[arg(long, short)]
45
    json: bool,
46

            
47
    /// Generate shell completion scripts for the specified shell
48
    /// and print them to stdout.
49
    #[arg(long, value_enum, hide = true)]
50
    completions: Option<Shell>,
51
}
52

            
53
/// Users idle for longer than this are omitted from the output by default.
54
const DEFAULT_IDLE_TIMEOUT_MINUTES: u32 = 60;
55

            
56
#[tokio::main]
57
async fn main() -> anyhow::Result<()> {
58
    let args = Args::parse();
59

            
60
    if let Some(shell) = args.completions {
61
        generate(shell, &mut Args::command(), "rwho", &mut std::io::stdout());
62
        return Ok(());
63
    }
64

            
65
    let max_idle_seconds = if args.all {
66
        None
67
    } else {
68
        Some(i64::from(args.idle_timeout) * 60)
69
    };
70

            
71
    let mut conn = zlink::tokio::unix::connect(DEFAULT_CLIENT_SERVER_SOCKET_PATH)
72
        .await
73
        .expect("Failed to connect to rwhod server");
74

            
75
    let reply = conn
76
        .rwho(max_idle_seconds)
77
        .await
78
        .context("Failed to send rwho request")?
79
        .map_err(|e| match e {
80
            VarlinkRwhodClientError::Disabled => {
81
                anyhow::anyhow!("The rwhod service is disabled on the server")
82
            }
83
            VarlinkRwhodClientError::TimedOut => {
84
                anyhow::anyhow!("The rwhod service timed out while processing the request")
85
            }
86
            VarlinkRwhodClientError::InvalidRequest => {
87
                anyhow::anyhow!("The rwhod service could not process the request, please check the logs or report the error to your system administrators")
88
            }
89
        })?;
90

            
91
    if args.json {
92
        let sorted: BTreeMap<String, Vec<WhodUserEntry>> = reply
93
            .into_iter()
94
            .map(|(hostname, mut users)| {
95
                users.sort_by(|a, b| a.user_id.cmp(&b.user_id).then_with(|| a.tty.cmp(&b.tty)));
96
                (hostname, users)
97
            })
98
            .collect();
99
        println!("{}", serde_json::to_string_pretty(&sorted).unwrap());
100
    } else {
101
        let mut entries: Vec<(String, WhodUserEntry)> = reply
102
            .into_iter()
103
            .flat_map(|(hostname, users)| {
104
                users.into_iter().map(move |user| (hostname.clone(), user))
105
            })
106
            .collect();
107

            
108
        entries.sort_by(|(host, user), (host2, user2)| {
109
            user.user_id
110
                .cmp(&user2.user_id)
111
                .then_with(|| host.cmp(host2))
112
                .then_with(|| user.tty.cmp(&user2.tty))
113
        });
114

            
115
        if args.old {
116
            old_format_user_entries(&entries)
117
                .iter()
118
                .for_each(|line| println!("{}", line));
119
        } else {
120
            // TODO: add a newer and nicer format here
121
            old_format_user_entries(&entries)
122
                .iter()
123
                .for_each(|line| println!("{}", line));
124
        }
125
    }
126

            
127
    Ok(())
128
}
129

            
130
fn old_format_user_entries(entries: &[(String, WhodUserEntry)]) -> Vec<String> {
131
    let hostname_tty_width = entries
132
        .iter()
133
        .map(|(host, user)| host.len() + user.tty.len() + 1)
134
        .max()
135
        .unwrap_or(0);
136

            
137
    let idle_time_width = entries
138
        .iter()
139
        .map(|(_, user)| user.idle_time.num_hours())
140
        .max()
141
        .map(|hours| {
142
            if hours >= 10 {
143
                5
144
            } else if hours > 0 {
145
                4
146
            } else {
147
                3
148
            }
149
        })
150
        .unwrap_or(0);
151

            
152
    entries
153
        .iter()
154
        .map(|(hostname, user)| {
155
            old_format_user_entry(hostname, hostname_tty_width, idle_time_width, user)
156
        })
157
        .collect()
158
}
159

            
160
fn old_format_user_entry(
161
    hostname: &str,
162
    hostname_tty_width: usize,
163
    idle_time_width: usize,
164
    user: &WhodUserEntry,
165
) -> String {
166
    let idle_str = {
167
        let hours = user.idle_time.num_hours().min(99);
168
        let minutes = user.idle_time.num_minutes() % 60;
169
        format!(
170
            "{}:{:02}",
171
            if hours == 0 {
172
                "".to_string()
173
            } else {
174
                hours.to_string()
175
            },
176
            minutes
177
        )
178
    };
179

            
180
    format!(
181
        "{:<8.8} {:<hostname_tty_width$} {:.12} {:>idle_time_width$}",
182
        user.user_id,
183
        format!("{hostname}:{}", user.tty),
184
        user.login_time.format("%b %d %H:%M"),
185
        idle_str,
186
    )
187
}