~goorzhel/radm

radm/src/cli.rs -rw-r--r-- 5.0 KiB
d11d708e — Antonio Gurgel 0.6.4 a month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
//! Command-line interface.

use std::{
    fs::{read_dir, OpenOptions},
    path::{Path, PathBuf},
};

use anyhow::{anyhow, Context, Result};
use clap::Parser;
use log::{debug, trace, LevelFilter};
use nix::unistd::{Gid, Group};
use simplelog::WriteLogger;
use syslog::{BasicLogger, Facility, Formatter3164};
use xdg::BaseDirectories;

/// The options set at the command line.
#[derive(Parser, Debug)]
#[clap(author, version, about = "A console-based display manager.")]
pub struct Options {
    // Don't forget to repeat changes here on `res/radm.1.md`.
    //
    // Imperative mood instead of present indicative to be consistent with clap's -h option.
    // (Honestly, I'd use imperative everywhere, but the `std` crate doesn't, so I decided
    // to be consistent with that.)
    /// Focus the given TTY when radm starts.
    #[clap(short, long, name = "TTY")]
    pub focus_on_start: Option<i32>,
    /// Enable DEBUG-level logging (twice for TRACE).
    #[clap(short, long, parse(from_occurrences))]
    pub verbose: u8,
    /// Use `dbus-run-session` to start the desktop.
    #[clap(short, long, parse(from_flag))]
    pub dbus: Dbus,
    /// Include this directory when searching for sessions (can be used more than once).
    #[clap(short, long, name = "DIR")]
    pub session_dirs: Vec<PathBuf>,
    /// Log to this path instead of syslog.
    #[clap(short, long, name = "PATH")]
    pub log_file: Option<String>,
    /// Omit password authentication for users in this group.
    #[clap(short, long, name = "GROUP_NAME")]
    pub autologin_group: Option<String>,
}

impl Options {
    pub fn new() -> Self {
        let mut out = Options::parse();

        /// Return each of the XDG_DATA_DIRS entries with `leaf_dir` appended, so long as the
        /// resulting path exists.
        ///
        /// If radm's user (usually `root`) has XDG_DATA_DIRS set, this function
        /// will use whatever's in it.
        ///
        /// Function exists because firing off a "Couldn't read
        /// /usr/local/share/wayland-sessions: no such directory" error line
        /// from sessions::find_sessions on every startup would be rude,
        /// assuming only `/usr/share/w...` exists.
        fn gather_default_dirs(leaf_dir: &str) -> impl Iterator<Item = PathBuf> + '_ {
            let xdg = BaseDirectories::new().unwrap();
            // unwrap: there's an error if and only if:
            // 1. $HOME is unset, AND
            // 2. radm's user -- usually `root` -- has no homedir.
            xdg.get_data_dirs().into_iter().filter_map(move |mut p| {
                p.push(leaf_dir);
                trace!("Checking existence of {}", p.to_string_lossy());
                match read_dir(&p) {
                    Ok(_) => Some(p),
                    Err(_) => None,
                }
            })
        }

        out.session_dirs = out
            .session_dirs
            .into_iter()
            .chain(gather_default_dirs("wayland-sessions"))
            .collect::<Vec<_>>();
        out
    }

    fn verbosity(&self) -> LevelFilter {
        match self.verbose {
            0 => LevelFilter::Info,
            1 => LevelFilter::Debug,
            _ => LevelFilter::Trace,
        }
    }

    pub fn autologin_gid(&self) -> Result<Option<Gid>> {
        if self.autologin_group.is_none() {
            return Ok(None);
        }
        match Group::from_name(self.autologin_group.as_ref().unwrap())? {
            // unwrap: autologin_group has just been proven to be Some.
            Some(g) => Ok(Some(g.gid)),
            None => return Err(anyhow!("Autologin group doesn't exist")),
        }
    }
}

/// Connects to a running syslog instance.
pub fn init_syslog(verbosity: LevelFilter) -> Result<()> {
    let formatter = Formatter3164 {
        facility: Facility::LOG_AUTH,
        hostname: None,
        process: "radm".into(),
        pid: std::process::id(),
    };
    let logger =
        syslog::unix(formatter).map_err(|e| anyhow!("Couldn't connect to syslog: {}", e))?;
    log::set_boxed_logger(Box::new(BasicLogger::new(logger)))?;
    log::set_max_level(verbosity);
    debug!("Syslog initialized");
    Ok(())
}

/// Initializes a file as a log sink.
pub fn init_log_file(path: &Path, verbosity: LevelFilter) -> Result<()> {
    let file = OpenOptions::new()
        .append(true)
        .create(true)
        .open(&path)
        .with_context(|| anyhow!("Couldn't open {}", &path.display()))?;
    WriteLogger::init(verbosity, Default::default(), file)?;
    debug!("Logfile {} initialized", path.display());
    Ok(())
}

/// Initializes either a log file or a syslog connection.
pub fn init_logger(opts: &Options) -> Result<()> {
    match &opts.log_file {
        Some(path) => init_log_file(Path::new(&path), opts.verbosity()),
        None => init_syslog(opts.verbosity()),
    }
}

#[derive(clap::ArgEnum, Debug, Clone)]
/// Whether a session should be run with D-Bus.
pub enum Dbus {
    No,
    Yes,
}

impl From<bool> for Dbus {
    fn from(b: bool) -> Self {
        match b {
            false => Dbus::No,
            true => Dbus::Yes,
        }
    }
}