1
use std::{
2
    io::{self, Read as _},
3
    os::fd::AsFd,
4
};
5

            
6
use anyhow::Context;
7
use clap::{CommandFactory, Parser};
8
use clap_complete::{Shell, generate};
9
use nix::unistd;
10
use roowho2_lib::{
11
    server::varlink_api::{
12
        DEFAULT_CLIENT_SERVER_SOCKET_PATH, VarlinkWalldClientProxy, VarlinkWalldClientResponse,
13
    },
14
    version,
15
};
16

            
17
/// Write a message to all users
18
#[derive(Debug, Parser)]
19
#[command(
20
  author = "Programvareverkstedet <projects@pvv.ntnu.no>",
21
  version,
22
  long_version = version::LONG_VERSION
23
)]
24
pub struct Args {
25
    /// Only send message to group
26
    #[arg(long, short, value_name = "GROUP")]
27
    group: Option<String>,
28

            
29
    // TODO: this 'works only for root' is leftover from the original wall implementation, no?
30
    //       maybe add it to the integration test?
31
    /// Do not print banner, works only for root
32
    #[arg(long, short)]
33
    nobanner: bool,
34

            
35
    /// Write timeout in seconds
36
    #[arg(long, short, value_name = "TIMEOUT", default_value_t = 30)]
37
    timeout: u32,
38

            
39
    /// Message to send, if not specified, read from stdin
40
    #[arg(value_name = "MESSAGE | FILE")]
41
    file_or_message: Option<String>,
42

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

            
49
#[tokio::main(flavor = "current_thread")]
50
async fn main() -> anyhow::Result<()> {
51
    let args = Args::parse();
52

            
53
    if let Some(shell) = args.completions {
54
        generate(shell, &mut Args::command(), "wall", &mut std::io::stdout());
55
        return Ok(());
56
    }
57

            
58
    let message = read_message(args.file_or_message.as_deref())?;
59

            
60
    let mut conn = zlink::tokio::unix::connect(DEFAULT_CLIENT_SERVER_SOCKET_PATH)
61
        .await
62
        .with_context(|| {
63
            format!("failed to connect to roowho2 at {DEFAULT_CLIENT_SERVER_SOCKET_PATH}")
64
        })?;
65

            
66
    let stdin = io::stdin();
67
    let source_tty = if !unistd::isatty(stdin.as_fd()).unwrap_or(false) {
68
        None
69
    } else {
70
        // TODO: should we send this as a CStr maybe?
71
        unistd::ttyname(stdin.as_fd())
72
            .ok()
73
            .map(|p| p.to_string_lossy().to_string())
74
    };
75

            
76
    let response = conn
77
        .wall(source_tty, message, args.group, args.nobanner, args.timeout)
78
        .await
79
        .context("varlink call to walld failed")?
80
        .map_err(|err| anyhow::format_err!("{err}"))
81
        .and_then(|res| {
82
            if let VarlinkWalldClientResponse::Wall(r) = res {
83
                Ok(r)
84
            } else {
85
                Err(anyhow::format_err!(
86
                    "unexpected response from walld: {:?}",
87
                    res
88
                ))
89
            }
90
        })?;
91

            
92
    for failure in &response.failures {
93
        eprintln!(
94
            "wall: could not reach {} on {}: {}",
95
            failure.user, failure.tty, failure.reason
96
        );
97
    }
98

            
99
    if !response.failures.is_empty() && response.delivered.is_empty() {
100
        std::process::exit(1);
101
    }
102

            
103
    Ok(())
104
}
105

            
106
fn read_message(file_or_message: Option<&str>) -> anyhow::Result<String> {
107
    match file_or_message {
108
        Some(arg) if std::path::Path::new(arg).is_file() => {
109
            std::fs::read_to_string(arg).with_context(|| format!("cannot read {arg}"))
110
        }
111
        Some(text) => Ok(text.to_string()),
112
        None => {
113
            let mut buf = String::new();
114
            std::io::stdin()
115
                .read_to_string(&mut buf)
116
                .context("failed to read message from stdin")?;
117
            Ok(buf)
118
        }
119
    }
120
}