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::{proto::WhodStatusUpdate, server::varlink_api::VarlinkRwhodClientProxy};
7

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

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

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

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

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

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

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

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

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

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

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

            
73
    let mut reply = conn
74
        .ruptime()
75
        .await
76
        .context("Failed to send rwho request")?
77
        .map_err(|e| anyhow::anyhow!("Server returned an error for rwho request: {:?}", e))?;
78

            
79
    sort_entries(&mut reply, args.load, args.time, args.users, args.reverse);
80

            
81
    if args.json {
82
        println!("{}", serde_json::to_string_pretty(&reply).unwrap());
83
    // } else if args.old {
84
    //     for entry in &reply {
85
    //         let line = old_format_machine_entry(args.all, entry);
86
    //         println!("{}", line);
87
    //     }
88
    } else {
89
        for entry in &reply {
90
            let line = old_format_machine_entry(args.all, entry);
91
            println!("{}", line);
92
        }
93
    }
94

            
95
    Ok(())
96
}
97

            
98
fn sort_entries(
99
    entries: &mut [WhodStatusUpdate],
100
    sort_by_load: bool,
101
    sort_by_time: bool,
102
    sort_by_users: bool,
103
    reverse: bool,
104
) {
105
    entries.sort_by(|entry1, entry2| {
106
        let ordering = if sort_by_load {
107
            let load1 = entry1.load_average.0 + entry1.load_average.1 + entry1.load_average.2;
108
            let load2 = entry2.load_average.0 + entry2.load_average.1 + entry2.load_average.2;
109
            load1
110
                .partial_cmp(&load2)
111
                .unwrap_or(std::cmp::Ordering::Equal)
112
        } else if sort_by_time {
113
            let uptime1 = Utc::now() - entry1.sendtime;
114
            let uptime2 = Utc::now() - entry2.sendtime;
115
            uptime1.cmp(&uptime2)
116
        } else if sort_by_users {
117
            let users1 = entry1.users.len();
118
            let users2 = entry2.users.len();
119
            users1.cmp(&users2)
120
        } else {
121
            entry1.hostname.cmp(&entry2.hostname)
122
        };
123

            
124
        if reverse {
125
            ordering.reverse()
126
        } else {
127
            ordering
128
        }
129
    });
130
}
131

            
132
fn old_format_machine_entry(all: bool, entry: &WhodStatusUpdate) -> String {
133
    let time_since_last_ping = Utc::now() - entry.sendtime;
134
    let is_up = time_since_last_ping <= Duration::minutes(11);
135

            
136
    let uptime = Utc::now() - entry.boot_time;
137
    let days = uptime.num_days();
138
    let hours = uptime.num_hours() % 24;
139
    let minutes = uptime.num_minutes() % 60;
140

            
141
    let uptime_str = if days > 0 {
142
        format!("{:3}+{:02}:{:02}", days, hours, minutes)
143
    } else if uptime.num_seconds() < 0 || days > 999 {
144
        "    ??:??".to_string()
145
    } else {
146
        format!("    {:2}:{:02}", hours, minutes)
147
    };
148

            
149
    let user_count = if all {
150
        entry.users.len()
151
    } else {
152
        entry
153
            .users
154
            .iter()
155
            .filter(|user| user.idle_time < Duration::hours(1))
156
            .count()
157
    };
158

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