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.
This commit is contained in:
Kenny Levinsen
2020-09-08 17:57:34 +02:00
parent c71b83eca7
commit 3dccaa44c7
5 changed files with 190 additions and 70 deletions

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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(())

View File

@@ -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<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)?;
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),

View File

@@ -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.