~kennylevinsen/greetd

3dccaa44c7452d9687944e0be8615af11380f198 — Kenny Levinsen 22 days ago c71b83e master
Use stdin as VT for current/none vt selections

The controlling tty will now be obtained from stdin if possible for
current/non, which is useful for inittab setups and shell test
use-cases.
M greetd/src/context.rs => greetd/src/context.rs +7 -9
@@ 10,7 10,7 @@ use crate::{
    error::Error,
    session::{
        interface::{Session, SessionChild, SessionState},
        worker::AuthMessageType as SessAuthMessageType,
        worker::{AuthMessageType as SessAuthMessageType, TerminalMode},
    },
};
use greetd_ipc::AuthMessageType;


@@ 37,16 37,16 @@ pub struct Context {
    inner: RwLock<ContextInner>,
    greeter_bin: String,
    greeter_user: String,
    vt: usize,
    pam_service: String,
    term_mode: TerminalMode,
}

impl Context {
    pub fn new(
        greeter_bin: String,
        greeter_user: String,
        vt: usize,
        pam_service: String,
        term_mode: TerminalMode,
    ) -> Context {
        Context {
            inner: RwLock::new(ContextInner {


@@ 56,8 56,8 @@ impl Context {
            }),
            greeter_bin,
            greeter_user,
            vt,
            pam_service,
            term_mode,
        }
    }



@@ 72,7 72,7 @@ impl Context {
    ) -> Result<SessionChild, Error> {
        let mut scheduled_session = Session::new_external()?;
        scheduled_session
            .initiate(&self.pam_service, class, user, false, self.vt)
            .initiate(&self.pam_service, class, user, false, &self.term_mode)
            .await?;
        loop {
            match scheduled_session.get_state().await {


@@ 157,7 157,7 @@ impl Context {
        };
        session_set
            .session
            .initiate(&self.pam_service, "user", &username, true, self.vt)
            .initiate(&self.pam_service, "user", &username, true, &self.term_mode)
            .await?;

        let mut session = Some(session_set);


@@ 305,9 305,7 @@ impl Context {
                            drop(inner);
                            let s = match scheduled.session.start().await {
                                Ok(s) => s,
                                Err(e) => {
                                    return Err(format!("session start failed: {}", e).into());
                                }
                                Err(e) => return Err(format!("session start failed: {}", e).into()),
                            };
                            let mut inner = self.inner.write().await;
                            inner.current = Some(SessionChildSet {

M greetd/src/server.rs => greetd/src/server.rs +92 -19
@@ 11,6 11,7 @@ use crate::{
    config::{Config, VtSelection},
    context::Context,
    error::Error,
    session::worker::TerminalMode,
    terminal::{self, Terminal},
};
use greetd_ipc::{


@@ 18,10 19,15 @@ use greetd_ipc::{
    ErrorType, Request, Response,
};

fn reset_vt(vt: usize) -> Result<(), Error> {
    let term = Terminal::open(vt)?;
    term.kd_setmode(terminal::KdMode::Text)?;
    term.vt_setactivate(vt)?;
fn reset_vt(term_mode: &TerminalMode) -> Result<(), Error> {
    match term_mode {
        TerminalMode::Terminal { path, vt, .. } => {
            let term = Terminal::open(path)?;
            term.kd_setmode(terminal::KdMode::Text)?;
            term.vt_setactivate(*vt)?;
        }
        TerminalMode::Stdin => (),
    }
    Ok(())
}



@@ 76,6 82,84 @@ async fn client_handler(ctx: &Context, mut s: UnixStream) -> Result<(), Error> {
    }
}

// Return a TTY path and the TTY/VT number, based on the configured target.
//
// If the target is VtSelection::Current, return the path to the TTY
// referenced by stdin and the TTY number it is connected to if possible. If
// the referenced TTY is a PTY, fail. Otherwise, open tty0, get the current VT
// number, and return the path to that TTY and VT.
//
// If the target is VtSelection::Next, open tty0 and request the next VT
// number. Return the TTY and VT
//
// If the target is VtSelection::Specific, simply return the specified TTY and
// VT.
//
// If the target is VtSelection::None, return nothing.
fn get_tty(config: &Config) -> Result<TerminalMode, Error> {
    const TTY_PREFIX: &str = "/dev/tty";
    const PTS_PREFIX: &str = "/dev/pts";

    let term = match config.file.terminal.vt {
        VtSelection::Current => {
            let term = Terminal::stdin();
            match term.ttyname() {
                // We have a usable terminal, so let's decipher and return that
                Ok(term_name)
                    if term_name.starts_with(TTY_PREFIX) && term_name.len() > TTY_PREFIX.len() =>
                {
                    let vt = term_name[TTY_PREFIX.len()..]
                        .parse()
                        .map_err(|e| Error::Error(format!("unable to parse tty number: {}", e)))?;

                    TerminalMode::Terminal {
                        path: term_name,
                        vt,
                        switch: false,
                    }
                }
                Ok(term_name) if term_name.starts_with(PTS_PREFIX) => {
                    return Err("cannot use current VT when started from a psuedo terminal".into())
                }
                // We don't have a usable terminal, so we have to jump through some hoops
                _ => {
                    let sys_term = Terminal::open("/dev/tty0")
                        .map_err(|e| format!("unable to open terminal: {}", e))?;
                    let vt = sys_term
                        .vt_get_current()
                        .map_err(|e| format!("unable to get current VT: {}", e))?;
                    TerminalMode::Terminal {
                        path: format!("/dev/tty{}", vt),
                        vt,
                        switch: false,
                    }
                }
            }
        }
        VtSelection::Next => {
            let term = Terminal::open("/dev/tty0")
                .map_err(|e| format!("unable to open terminal: {}", e))?;
            let vt = term
                .vt_get_next()
                .map_err(|e| format!("unable to get next VT: {}", e))?;
            TerminalMode::Terminal {
                path: format!("/dev/tty{}", vt),
                vt,
                switch: true,
            }
        }
        VtSelection::None => TerminalMode::Stdin,
        VtSelection::Specific(vt) => TerminalMode::Terminal {
            path: format!("/dev/tty{}", vt),
            vt,
            switch: true,
        },
    };
    return Ok(term);
}

// Listener is a convenience wrapper for creating the UnixListener we need, and
// for providing cleanup on Drop.
struct Listener(UnixListener);

impl Listener {


@@ 123,36 207,25 @@ pub async fn main(config: Config) -> Result<(), Error> {

    let mut listener = Listener::create(uid, gid)?;

    let term = Terminal::open(0).map_err(|e| format!("unable to open terminal: {}", e))?;
    let vt = match config.file.terminal.vt {
        VtSelection::Current => term
            .vt_get_current()
            .map_err(|e| format!("unable to get current VT: {}", e))?,
        VtSelection::Next => term
            .vt_get_next()
            .map_err(|e| format!("unable to get next VT: {}", e))?,
        VtSelection::None => 0,
        VtSelection::Specific(v) => v,
    };
    drop(term);
    let term_mode = get_tty(&config)?;

    let ctx = Rc::new(Context::new(
        config.file.default_session.command,
        config.file.default_session.user,
        vt,
        service.to_string(),
        term_mode.clone(),
    ));

    if let Some(s) = config.file.initial_session {
        if let Err(e) = ctx.start_user_session(&s.user, vec![s.command]).await {
            eprintln!("unable to start greeter: {}", e);
            reset_vt(vt).map_err(|e| format!("unable to reset VT: {}", e))?;
            reset_vt(&term_mode).map_err(|e| format!("unable to reset VT: {}", e))?;

            std::process::exit(1);
        }
    } else if let Err(e) = ctx.greet().await {
        eprintln!("unable to start greeter: {}", e);
        reset_vt(vt).map_err(|e| format!("unable to reset VT: {}", e))?;
        reset_vt(&term_mode).map_err(|e| format!("unable to reset VT: {}", e))?;

        std::process::exit(1);
    }

M greetd/src/session/interface.rs => greetd/src/session/interface.rs +6 -6
@@ 13,7 13,7 @@ use async_trait::async_trait;

use tokio::net::UnixDatagram as TokioUnixDatagram;

use super::worker::{AuthMessageType, ParentToSessionChild, SessionChildToParent};
use super::worker::{AuthMessageType, ParentToSessionChild, SessionChildToParent, TerminalMode};
use crate::error::Error;

#[async_trait]


@@ 54,14 54,14 @@ impl AsyncRecv<SessionChildToParent> for SessionChildToParent {

/// SessionChild tracks the processes spawned by a session
pub struct SessionChild {
    task: Pid,
    sub_task: Pid,
    pub task: Pid,
    pub sub_task: Pid,
}

impl SessionChild {
    /// Check if this session has this pid.
    pub fn owns_pid(&self, pid: Pid) -> bool {
        self.task == pid || self.sub_task == pid
        self.task == pid
    }

    /// Send SIGTERM to the session child.


@@ 136,14 136,14 @@ impl Session {
        class: &str,
        user: &str,
        authenticate: bool,
        vt: usize,
        term_mode: &TerminalMode,
    ) -> Result<(), Error> {
        let msg = ParentToSessionChild::InitiateLogin {
            service: service.to_string(),
            class: class.to_string(),
            user: user.to_string(),
            authenticate,
            vt,
            tty: term_mode.clone(),
        };
        msg.send(&mut self.sock).await?;
        Ok(())

M greetd/src/session/worker.rs => greetd/src/session/worker.rs +42 -32
@@ 23,13 23,23 @@ pub enum AuthMessageType {
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum TerminalMode {
    Terminal {
        path: String,
        vt: usize,
        switch: bool,
    },
    Stdin,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ParentToSessionChild {
    InitiateLogin {
        service: String,
        class: String,
        user: String,
        authenticate: bool,
        vt: usize,
        tty: TerminalMode,
    },
    PamResponse {
        resp: Option<String>,


@@ 70,14 80,14 @@ impl SessionChildToParent {
/// responsible for the entirety of the session setup and execution. It is
/// started by Session::start.
fn worker(sock: &UnixDatagram) -> Result<(), Error> {
    let (service, class, user, authenticate, vt) = match ParentToSessionChild::recv(sock)? {
    let (service, class, user, authenticate, tty) = match ParentToSessionChild::recv(sock)? {
        ParentToSessionChild::InitiateLogin {
            service,
            class,
            user,
            authenticate,
            vt,
        } => (service, class, user, authenticate, vt),
            tty,
        } => (service, class, user, authenticate, tty),
        ParentToSessionChild::Cancel => return Err("cancelled".into()),
        msg => return Err(format!("expected InitiateLogin or Cancel, got: {:?}", msg).into()),
    };


@@ 85,9 95,6 @@ fn worker(sock: &UnixDatagram) -> Result<(), Error> {
    let conv = Box::pin(SessionConv::new(sock));
    let mut pam = PamSession::start(&service, &user, conv)?;

    // Tell PAM what TTY we're targetting, which is used by logind.
    pam.set_item(PamItemType::TTY, &format!("tty{}", vt))?;

    if authenticate {
        pam.authenticate(PamFlag::NONE)?;
    }


@@ 122,33 129,37 @@ fn worker(sock: &UnixDatagram) -> Result<(), Error> {
    // Make this process a session leader.
    setsid().map_err(|e| format!("unable to become session leader: {}", e))?;

    // Opening our target terminal. This will automatically make it our
    // controlling terminal. An attempt was made to use TIOCSCTTY to do this
    // explicitly, but it neither worked nor was worth the additional code.
    let target_term = terminal::Terminal::open(vt)?;

    // Set the target VT mode to text for compatibility. Other login managers
    // set this to graphics, but that disallows start of textual applications,
    // which greetd aims to support.
    target_term.kd_setmode(terminal::KdMode::Text)?;

    // Clear TTY so that it will be empty when we switch to it.
    target_term.term_clear()?;
    match tty {
        TerminalMode::Stdin => (),
        TerminalMode::Terminal { path, vt, switch } => {
            // Tell PAM what TTY we're targetting, which is used by logind.
            pam.set_item(PamItemType::TTY, &format!("tty{}", vt))?;
            pam.putenv(&format!("XDG_VTNR={}", vt))?;

            // Opening our target terminal.
            let target_term = terminal::Terminal::open(&path)?;

            // Set the target VT mode to text for compatibility. Other login managers
            // set this to graphics, but that disallows start of textual applications,
            // which greetd aims to support.
            target_term.kd_setmode(terminal::KdMode::Text)?;

            // Clear TTY so that it will be empty when we switch to it.
            target_term.term_clear()?;

            // A bit more work if a VT switch is required.
            if switch && vt != target_term.vt_get_current()? {
                // Perform a switch to the target VT, simultaneously resetting it to
                // VT_AUTO.
                target_term.vt_setactivate(vt)?;
            }

    // A bit more work if a VT switch is required.
    if vt != 0 && vt != target_term.vt_get_current()? {
        // Perform a switch to the target VT, simultaneously resetting it to
        // VT_AUTO.
        target_term.vt_setactivate(vt)?;
            // Connect std(in|out|err), and make this our controlling TTY.
            target_term.term_connect_pipes()?;
            target_term.term_take_ctty()?;
        }
    }

    // Connect std(in|out|err), and make this our controlling TTY.
    target_term.term_connect_pipes()?;
    target_term.term_take_ctty()?;

    // We no longer need these, so close them to avoid inheritance.
    drop(target_term);

    // Prepare some values from the user struct we gathered earlier.
    let username = user.name().to_str().unwrap_or("");
    let home = user.home_dir().to_str().unwrap_or("");


@@ 174,7 185,6 @@ fn worker(sock: &UnixDatagram) -> Result<(), Error> {
    let prepared_env = [
        "XDG_SEAT=seat0".to_string(),
        format!("XDG_SESSION_CLASS={}", class),
        format!("XDG_VTNR={}", vt),
        format!("USER={}", username),
        format!("LOGNAME={}", username),
        format!("HOME={}", home),

M greetd/src/terminal/mod.rs => greetd/src/terminal/mod.rs +45 -6
@@ 6,7 6,7 @@ use nix::{
    sys::stat::Mode,
    unistd::{close, dup2, write},
};
use std::os::unix::io::RawFd;
use std::{ffi::CStr, os::unix::io::RawFd};

#[allow(dead_code)]
pub enum KdMode {


@@ 25,28 25,67 @@ impl KdMode {

pub struct Terminal {
    fd: RawFd,
    autoclose: bool,
}

impl Drop for Terminal {
    fn drop(&mut self) {
        close(self.fd).unwrap();
        if self.autoclose {
            close(self.fd).unwrap();
        }
    }
}

fn ttyname_r(fd: RawFd) -> Result<String, Error> {
    let mut arr: [u8; 32] = [0; 32];
    let res = unsafe {
        libc::ttyname_r(
            fd as libc::c_int,
            &mut arr[0] as *mut u8 as *mut libc::c_char,
            31,
        )
    };
    if res != 0 {
        return Err("ttyname_r failed".into());
    }
    let len = unsafe { libc::strnlen(&arr[0] as *const u8 as *const libc::c_char, 31) };
    let s = CStr::from_bytes_with_nul(&arr[..len + 1])
        .map_err(|e| Error::Error(format!("ttyname_r result conversion failed: {}", e)))?;
    Ok(s.to_str()
        .map_err(|e| Error::Error(format!("ttyname_r result conversion failed: {}", e)))?
        .to_string())
}

impl Terminal {
    /// Open the terminal file for the specified terminal number.
    pub fn open(terminal: usize) -> Result<Terminal, Error> {
    pub fn open(terminal: &str) -> Result<Terminal, Error> {
        let res = open(
            format!("/dev/tty{}", terminal).as_str(),
            terminal,
            OFlag::O_RDWR | OFlag::O_NOCTTY,
            Mode::from_bits_truncate(0o666),
        );
        match res {
            Ok(fd) => Ok(Terminal { fd }),
            Err(e) => Err(format!("terminal: unable to open: {}", e).into()),
            Ok(fd) => Ok(Terminal {
                fd,
                autoclose: true,
            }),
            Err(e) => return Err(format!("terminal: unable to open: {}", e).into()),
        }
    }

    /// Open the terminal from stdin
    pub fn stdin() -> Terminal {
        Terminal {
            fd: 0 as RawFd,
            autoclose: false,
        }
    }

    /// Returns the name of the TTY
    pub fn ttyname(&self) -> Result<String, Error> {
        ttyname_r(self.fd)
    }

    /// Set the kernel display to either graphics or text mode. Graphivs mode
    /// disables the kernel console on this VT, and also disables blanking
    /// between VT switches if both source and target VT is in graphics mode.