~goorzhel/radm

ref: b4ca8d0dde821420d66d488db969c0f7c5da5f6b radm/src/user.rs -rw-r--r-- 8.0 KiB
b4ca8d0d — Antonio Gurgel 0.3.0 4 months 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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
//! Post-authentication tasks.

use std::{
    fs::File,
    io::{Read, Write},
    process::{Command, ExitStatus},
    str::FromStr,
    thread::sleep,
    time::Duration,
};

use anyhow::{anyhow, Context, Error, Result};
use nix::unistd::{Gid, Uid};
use pwd::Passwd;
use xdg::BaseDirectories;

use crate::{
    cli::Dbus,
    session::Desktop,
    system::env_var_or_set_default,
    tui::{prompt, EchoInput},
};

/// An authenticated user.
pub struct User {
    pub name: String,
    pub uid: Uid,
    pub gid: Gid,
    pub dir: String,
    shell: String,
}

impl From<Passwd> for User {
    fn from(passwd_entry: Passwd) -> Self {
        Self {
            name: passwd_entry.name,
            uid: Uid::from_raw(passwd_entry.uid),
            gid: Gid::from_raw(passwd_entry.gid),
            dir: passwd_entry.dir,
            shell: passwd_entry.shell,
        }
    }
}

impl FromStr for User {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let passwd_entry = Passwd::from_name(s)?.unwrap();
        // Option unwrapped because users successfully authed can be assumed to exist in passwd.
        Ok(passwd_entry.into())
    }
}

impl User {
    /// Loads the user's environment by executing `env` through a login shell.
    ///
    /// The [return value](xdg::BaseDirectories) is useful for later functions to determine
    /// runtime, state, and cache storage. Accordingly, the env var `$XDG_RUNTIME_DIR`
    /// will default to `/run/user/$UID` if unset.
    ///
    /// Known to work with: bash, dash, and zsh.
    pub fn load_environment(&self) -> Result<BaseDirectories> {
        let sh = Command::new(&self.shell)
            .args(["-lc", "env"])
            .output()
            .context("Couldn't run user's shell")?;
        let output = String::from_utf8(sh.stdout).context("Found invalid UTF-8 in environment")?;

        for pair in output.split_terminator('\n') {
            let mut pair = pair.split('=');
            let (key, value) = (pair.next().unwrap(), pair.next().unwrap());
            // unwrap: output has already been ensured to be KV pairs in valid UTF-8.
            trace!("Loading environment: {}={}", key, value);
            std::env::set_var(key, value);
        }

        // `xdg` crate doesn't assume $XDG_RUNTIME_DIR. `rstdm` will default to `/run/user/$UID`
        // until there's reason to do otherwise.
        env_var_or_set_default("XDG_RUNTIME_DIR", format!("/run/user/{}", self.uid));

        Ok(BaseDirectories::new()?)
    }

    /// Opens handles on `$XDG_STATE_HOME/${SESSION_NAME}-std{out,err}`, for capturing output from the [session
    /// process](crate::session::Desktop::exec).
    pub fn get_standard_outputs(
        &self,
        session_name: &str,
        xdg: &BaseDirectories,
    ) -> Result<(File, File)> {
        let path = xdg.get_state_home();

        let open = |file: &str| -> Result<File> {
            let mut path = path.clone();
            path.push(file);
            if path.exists() {
                let mut old = path.clone();
                old.set_extension("old");
                std::fs::rename(&path, old)?;
            }
            let file = std::fs::OpenOptions::new()
                .write(true)
                .create(true)
                .open(&path)
                .with_context(|| anyhow!("Couldn't open {}", &path.to_string_lossy()))?;
            Ok(file)
        };

        let stdout = open(&format!("{}-stdout.log", session_name))?;
        let stderr = open(&format!("{}-stderr.log", session_name))?;
        Ok((stdout, stderr))
    }

    /// Executes the desktop environment.
    pub fn launch_session(
        &self,
        session: &Desktop,
        xdg: &BaseDirectories,
        dbus: Dbus,
    ) -> Result<ExitStatus> {
        let (stdout, stderr) = self
            .get_standard_outputs(&session.name, xdg)
            .context("Couldn't set up stdout/err")?;

        // TODO: This and the Desktop type assume that `Exec` has one and only one argument.
        // Which is the case with `sway` and `i3`, but I don't know about other desktops.
        let mut cmd = match dbus {
            Dbus::Yes => {
                // Can't one-line this (E0716).
                let mut cmd = Command::new("dbus-run-session");
                cmd.arg(&session.exec);
                cmd
            }
            Dbus::No => Command::new(&session.exec),
        };

        trace!("Launching session for {}: {:?}", self.name, &cmd);

        cmd.stdout(stdout)
            .stderr(stderr)
            .status()
            .with_context(|| anyhow!("Couldn't execute {:?}", &cmd))
    }

    /// Prompts user to choose a session from a numbered list.
    ///
    /// If `sessions` has one item, it will be returned automatically.
    ///
    /// # Panics
    ///
    /// Panics if `sessions` is empty. Providing a non-empty list is up to
    /// [`find_sessions`](crate::session::find_sessions).
    pub fn choose_session(&self, sessions: &[Desktop], xdg: &BaseDirectories) -> Result<Desktop> {
        if sessions.is_empty() {
            panic!("choose_session() cannot choose among zero sessions");
        }

        if sessions.len() == 1 {
            let session = sessions[0].clone();
            println!(
                "Found only one session ({}); starting it now.",
                session.name
            );
            sleep(Duration::from_secs(1));
            return Ok(session);
        }

        let mut cache = xdg.get_cache_home();
        cache.push("rstdm-last");
        let file = std::fs::OpenOptions::new()
            .read(true)
            .write(true)
            .create(true)
            .open(&cache)
            .map_err(|e| eprintln!("Couldn't open session cache: {}", e));
        // I'd love to use anyhow here but an error needn't be fatal to choose_session.
        let mut cached_name = String::new();
        if let Ok(mut file) = file {
            file.read_to_string(&mut cached_name)
                .map_err(|e| eprintln!("Couldn't read session cache: {}", e))
                .ok();
        }

        let mut cached_choice: Option<usize> = None;
        for (i, session) in sessions.iter().enumerate().map(|(i, s)| (i + 1, s)) {
            if session.name == cached_name {
                cached_choice = Some(i)
            }
            match &session.comment {
                // I thought about implementing this as Desktop::display, but this use case is too
                // specific to warrant that.
                Some(comment) => {
                    println!("{:2}. {:15} {}", i, &session.name, comment)
                }
                None => println!("{:2}. {}", i, &session.name),
            }
        }

        loop {
            let choice = match cached_choice {
                Some(c) => {
                    let input = prompt(format!("Desktop choice [{}]", c), EchoInput::Echo)?;
                    match input.is_empty() {
                        true => c.to_string(),
                        false => input,
                    }
                }
                None => prompt("Desktop choice", EchoInput::Echo)?,
            };
            if let Ok(i) = usize::from_str(&choice) {
                if let Some(desktop) = sessions.get(i - 1) {
                    let mut file = std::fs::OpenOptions::new()
                        .write(true)
                        .truncate(true)
                        .open(&cache)?;
                    file.write_all(desktop.name.as_bytes())
                        .map_err(|e| {
                            let e = format!("Couldn't cache session name: {}", e);
                            eprintln!("{}", e);
                            error!("{}", e);
                        })
                        .ok();

                    return Ok(desktop.clone());
                }
            }
        }
    }
}

#[cfg(test)]
mod test {
    use std::str::FromStr;

    use nix::unistd::{Gid, Uid};

    use super::User;

    #[test]
    fn user_fromstr_does_passwd_lookup() {
        let root = User::from_str("root").unwrap();
        assert_eq!(root.uid, Uid::from_raw(0));
        assert_eq!(root.gid, Gid::from_raw(0));
    }
}