1
use anyhow::Context;
2
use chrono::{Duration, Utc};
3
use clap::{CommandFactory, Parser};
4
use clap_complete::{Shell, generate};
5

            
6
use roowho2_lib::{
7
    proto::WhodStatusUpdate,
8
    server::varlink_api::{VarlinkRwhodClientError, VarlinkRwhodClientProxy},
9
};
10

            
11
/// Show host status of local machines.
12
///
13
/// `ruptime` gives a status line like uptime for each machine on the local network;
14
/// these are formed from packets broadcast by each host on the network once a minute.
15
///
16
/// Machines for which no status report has been received for 11 minutes are shown as being down.
17
#[derive(Debug, Parser)]
18
#[command(author = "Programvareverkstedet <projects@pvv.ntnu.no>", version)]
19
pub struct Args {
20
    /// Users idle an hour or more are not counted unless the `-a` flag is given.
21
    #[arg(long, short)]
22
    all: bool,
23

            
24
    /// Sort by load average.
25
    #[arg(long, short, conflicts_with = "time", conflicts_with = "users")]
26
    load: bool,
27

            
28
    /// Reverses the sort order.
29
    #[arg(long, short)]
30
    reverse: bool,
31

            
32
    /// Sort by uptime.
33
    #[arg(long, short, conflicts_with = "load", conflicts_with = "users")]
34
    time: bool,
35

            
36
    /// Sort by number of users.
37
    #[arg(long, short, conflicts_with = "load", conflicts_with = "time")]
38
    users: bool,
39

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

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

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

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

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

            
68
    let mut conn = zlink::unix::connect("/run/roowho2/roowho2.varlink")
69
        .await
70
        .expect("Failed to connect to rwhod server");
71

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

            
88
    sort_entries(&mut reply, args.load, args.time, args.users, args.reverse);
89

            
90
    if args.json {
91
        println!("{}", serde_json::to_string_pretty(&reply).unwrap());
92
    // } else if args.old {
93
    //     for entry in &reply {
94
    //         let line = old_format_machine_entry(args.all, entry);
95
    //         println!("{}", line);
96
    //     }
97
    } else {
98
        for entry in &reply {
99
            let line = old_format_machine_entry(entry);
100
            println!("{}", line);
101
        }
102
    }
103

            
104
    Ok(())
105
}
106

            
107
fn sort_entries(
108
    entries: &mut [WhodStatusUpdate],
109
    sort_by_load: bool,
110
    sort_by_time: bool,
111
    sort_by_users: bool,
112
    reverse: bool,
113
) {
114
    entries.sort_by(|entry1, entry2| {
115
        let ordering = if sort_by_load {
116
            let load1 = entry1.load_average.0 + entry1.load_average.1 + entry1.load_average.2;
117
            let load2 = entry2.load_average.0 + entry2.load_average.1 + entry2.load_average.2;
118
            load1
119
                .partial_cmp(&load2)
120
                .unwrap_or(std::cmp::Ordering::Equal)
121
        } else if sort_by_time {
122
            let uptime1 = Utc::now() - entry1.sendtime;
123
            let uptime2 = Utc::now() - entry2.sendtime;
124
            uptime1.cmp(&uptime2)
125
        } else if sort_by_users {
126
            let users1 = entry1.users.len();
127
            let users2 = entry2.users.len();
128
            users1.cmp(&users2)
129
        } else {
130
            entry1.hostname.cmp(&entry2.hostname)
131
        };
132

            
133
        if reverse {
134
            ordering.reverse()
135
        } else {
136
            ordering
137
        }
138
    });
139
}
140

            
141
fn old_format_machine_entry(entry: &WhodStatusUpdate) -> String {
142
    let time_since_last_ping = Utc::now() - entry.sendtime;
143
    let is_up = time_since_last_ping <= Duration::minutes(11);
144

            
145
    let uptime = Utc::now() - entry.boot_time;
146
    let days = uptime.num_days();
147
    let hours = uptime.num_hours() % 24;
148
    let minutes = uptime.num_minutes() % 60;
149

            
150
    let uptime_str = if days > 0 {
151
        format!("{:3}+{:02}:{:02}", days, hours, minutes)
152
    } else if uptime.num_seconds() < 0 || days > 999 {
153
        "    ??:??".to_string()
154
    } else {
155
        format!("    {:2}:{:02}", hours, minutes)
156
    };
157

            
158
    let user_count = entry.users.len();
159

            
160
    format!(
161
        "{:<12.12} {} {},  {:4} user{}  load {:>4.2}, {:>4.2}, {:>4.2}",
162
        entry.hostname,
163
        if is_up { "up" } else { "down" },
164
        uptime_str,
165
        user_count,
166
        if user_count == 1 { ", " } else { "s," },
167
        entry.load_average.0 as f32 / 100.0,
168
        entry.load_average.1 as f32 / 100.0,
169
        entry.load_average.2 as f32 / 100.0,
170
    )
171
}