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(author = "Programvareverkstedet <projects@pvv.ntnu.no>", version)]
16
pub struct Args {
17
    /// Users idle an hour or more are not counted unless the `-a` flag is given.
18
    #[arg(long, short)]
19
    all: bool,
20

            
21
    /// Sort by load average.
22
    #[arg(long, short, conflicts_with = "time", conflicts_with = "users")]
23
    load: bool,
24

            
25
    /// Reverses the sort order.
26
    #[arg(long, short)]
27
    reverse: bool,
28

            
29
    /// Sort by uptime.
30
    #[arg(long, short, conflicts_with = "load", conflicts_with = "users")]
31
    time: bool,
32

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

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

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

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

            
51
#[tokio::main]
52
async fn main() -> anyhow::Result<()> {
53
    let args = Args::parse();
54

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

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

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

            
75
    sort_entries(&mut reply, args.load, args.time, args.users, args.reverse);
76

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

            
91
    Ok(())
92
}
93

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

            
120
        if reverse {
121
            ordering.reverse()
122
        } else {
123
            ordering
124
        }
125
    });
126
}
127

            
128
fn old_format_machine_entry(all: bool, entry: &WhodStatusUpdate) -> String {
129
    let time_since_last_ping = Utc::now() - entry.sendtime;
130
    let is_up = time_since_last_ping <= Duration::minutes(11);
131

            
132
    let uptime = Utc::now() - entry.boot_time;
133
    let days = uptime.num_days();
134
    let hours = uptime.num_hours() % 24;
135
    let minutes = uptime.num_minutes() % 60;
136

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

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

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