diff --git a/greetd/src/context.rs b/greetd/src/context.rs index f052574..0299ea8 100644 --- a/greetd/src/context.rs +++ b/greetd/src/context.rs @@ -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, 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 { 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 { diff --git a/greetd/src/server.rs b/greetd/src/server.rs index bed6752..e734eea 100644 --- a/greetd/src/server.rs +++ b/greetd/src/server.rs @@ -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 { + 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); } diff --git a/greetd/src/session/interface.rs b/greetd/src/session/interface.rs index 7d3e289..d6f6e8c 100644 --- a/greetd/src/session/interface.rs +++ b/greetd/src/session/interface.rs @@ -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 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(()) diff --git a/greetd/src/session/worker.rs b/greetd/src/session/worker.rs index baf517f..9687374 100644 --- a/greetd/src/session/worker.rs +++ b/greetd/src/session/worker.rs @@ -22,6 +22,16 @@ pub enum AuthMessageType { Error, } +#[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 { @@ -29,7 +39,7 @@ pub enum ParentToSessionChild { class: String, user: String, authenticate: bool, - vt: usize, + tty: TerminalMode, }, PamResponse { resp: Option, @@ -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)?; + 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))?; - // 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)?; + // Opening our target terminal. + let target_term = terminal::Terminal::open(&path)?; - // Clear TTY so that it will be empty when we switch to it. - target_term.term_clear()?; + // 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)?; - // 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)?; + // 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)?; + } + + // 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), diff --git a/greetd/src/terminal/mod.rs b/greetd/src/terminal/mod.rs index 18f6930..1da4904 100644 --- a/greetd/src/terminal/mod.rs +++ b/greetd/src/terminal/mod.rs @@ -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 { + 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 { + pub fn open(terminal: &str) -> Result { 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 { + 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.