use std::fs::{File, OpenOptions};
use std::io;
use std::os::fd::AsRawFd;
use std::sync::atomic::{AtomicBool, Ordering};
const KDSETMODE: libc::c_ulong = 0x4b3a;
const KDGETMODE: libc::c_ulong = 0x4b3b;
const KD_TEXT: libc::c_int = 0;
const KD_GRAPHICS: libc::c_int = 1;
const VT_GETMODE: libc::c_ulong = 0x5601;
const VT_SETMODE: libc::c_ulong = 0x5602;
const VT_GETSTATE: libc::c_ulong = 0x5603;
const VT_RELDISP: libc::c_ulong = 0x5605;
const VT_AUTO: u8 = 0;
const VT_PROCESS: u8 = 1;
const VT_ACKACQ: libc::c_int = 2;
#[repr(C)]
#[derive(Clone, Copy, Default)]
struct VtMode {
mode: u8,
waitv: u8,
relsig: i16,
acqsig: i16,
frsig: i16,
}
#[repr(C)]
#[derive(Default)]
struct VtState {
active: u16,
signal: u16,
state: u16,
}
static STOP: AtomicBool = AtomicBool::new(false);
static RELEASE: AtomicBool = AtomicBool::new(false);
static ACQUIRE: AtomicBool = AtomicBool::new(false);
pub fn stopping() -> bool {
STOP.load(Ordering::Acquire)
}
extern "C" fn signal_handler(signal: libc::c_int) {
if signal == libc::SIGUSR1 {
RELEASE.store(true, Ordering::Relaxed);
} else if signal == libc::SIGUSR2 {
ACQUIRE.store(true, Ordering::Relaxed);
}
STOP.store(true, Ordering::Release);
}
struct SignalHandlers {
saved: Vec<(libc::c_int, libc::sigaction)>,
}
impl SignalHandlers {
fn install() -> io::Result<Self> {
STOP.store(false, Ordering::Relaxed);
RELEASE.store(false, Ordering::Relaxed);
ACQUIRE.store(false, Ordering::Relaxed);
let mut handlers = Self { saved: Vec::new() };
for signal in [
libc::SIGINT,
libc::SIGTERM,
libc::SIGHUP,
libc::SIGQUIT,
libc::SIGTSTP,
libc::SIGUSR1,
libc::SIGUSR2,
] {
let mut action: libc::sigaction = unsafe { std::mem::zeroed() };
action.sa_sigaction = signal_handler as *const () as usize;
let mut old: libc::sigaction = unsafe { std::mem::zeroed() };
unsafe { libc::sigemptyset(&mut action.sa_mask) };
check(unsafe { libc::sigaction(signal, &action, &mut old) })?;
handlers.saved.push((signal, old));
}
Ok(handlers)
}
}
impl Drop for SignalHandlers {
fn drop(&mut self) {
for (signal, action) in self.saved.iter().rev() {
unsafe { libc::sigaction(*signal, action, std::ptr::null_mut()) };
}
}
}
struct TerminalInput {
tty: File,
saved: libc::termios,
}
impl TerminalInput {
fn acquire(tty: File) -> io::Result<Self> {
let fd = tty.as_raw_fd();
let mut saved = unsafe { std::mem::zeroed() };
check(unsafe { libc::tcgetattr(fd, &mut saved) })?;
check(unsafe { libc::tcsetattr(fd, libc::TCSANOW, &input_mode(saved)) })?;
Ok(Self { tty, saved })
}
}
impl Drop for TerminalInput {
fn drop(&mut self) {
let fd = self.tty.as_raw_fd();
unsafe { libc::tcflush(fd, libc::TCIFLUSH) };
report("restore terminal input", unsafe {
libc::tcsetattr(fd, libc::TCSANOW, &self.saved)
});
}
}
fn input_mode(mut mode: libc::termios) -> libc::termios {
mode.c_iflag &= !(libc::IXON | libc::IXOFF);
mode.c_lflag &= !(libc::ECHO | libc::ECHONL | libc::ICANON);
mode.c_lflag |= libc::ISIG;
mode.c_cc[libc::VMIN] = 1;
mode.c_cc[libc::VTIME] = 0;
mode
}
pub struct Console {
terminal_input: Option<TerminalInput>,
vt: Option<VirtualTerminal>,
_handlers: SignalHandlers,
}
struct VirtualTerminal {
tty: File,
termios: libc::termios,
vt_mode: VtMode,
graphics_mode: libc::c_int,
termios_changed: bool,
vt_changed: bool,
graphics_changed: bool,
}
impl Console {
pub fn owns_vt(&self) -> bool {
self.vt.is_some()
}
pub fn acquire() -> io::Result<Self> {
let original = std::env::var("SUDO_TTY")
.ok()
.filter(|path| (unsafe { libc::geteuid() }) == 0 && console_path(path));
let path = original.as_deref().unwrap_or("/dev/tty");
let tty = match OpenOptions::new().read(true).write(true).open(path) {
Ok(tty) => tty,
Err(error) if original.is_none() && error.raw_os_error() == Some(libc::ENXIO) => {
eprintln!("console: no controlling terminal; direct KMS without VT ownership");
return Ok(Self {
terminal_input: None,
vt: None,
_handlers: SignalHandlers::install()?,
});
}
Err(error) => return Err(error),
};
Self::acquire_tty(tty, original.is_some())
}
fn acquire_tty(tty: File, sudo_proxy: bool) -> io::Result<Self> {
let fd = tty.as_raw_fd();
let mut device: libc::c_uint = 0;
check(unsafe { libc::ioctl(fd, libc::TIOCGDEV, &mut device) })?;
let mut console = Self {
terminal_input: None,
vt: None,
_handlers: SignalHandlers::install()?,
};
if let Some(number) = vt_number(device as libc::dev_t) {
console.vt = Some(VirtualTerminal::acquire(tty, number, sudo_proxy)?);
if sudo_proxy {
let tty = OpenOptions::new().read(true).write(true).open("/dev/tty")?;
console.terminal_input = Some(TerminalInput::acquire(tty)?);
}
} else {
eprintln!("console: {}; direct KMS without VT ownership", terminal_name(fd));
if terminal_is_foreground(fd) {
console.terminal_input = Some(TerminalInput::acquire(tty)?);
}
}
Ok(console)
}
}
impl Drop for Console {
fn drop(&mut self) {
drop(self.terminal_input.take());
drop(self.vt.take());
}
}
fn vt_number(device: libc::dev_t) -> Option<u32> {
let number = libc::minor(device);
(libc::major(device) == 4 && (1..=63).contains(&number)).then_some(number)
}
pub fn terminal_is_foreground(fd: libc::c_int) -> bool {
unsafe { libc::tcgetpgrp(fd) == libc::getpgrp() }
}
impl VirtualTerminal {
fn acquire(tty: File, number: u32, sudo_proxy: bool) -> io::Result<Self> {
let fd = tty.as_raw_fd();
if !sudo_proxy && !terminal_is_foreground(fd) {
return Err(io::Error::other("run medias in the console foreground"));
}
let mut state = VtState::default();
check(unsafe { libc::ioctl(fd, VT_GETSTATE, &mut state) })?;
if u32::from(state.active) != number {
return Err(io::Error::other("the controlling console VT is not active"));
}
let mut vt_mode = VtMode::default();
check(unsafe { libc::ioctl(fd, VT_GETMODE, &mut vt_mode) })?;
let mut graphics_mode = KD_TEXT;
check(unsafe { libc::ioctl(fd, KDGETMODE, &mut graphics_mode) })?;
if vt_mode.mode != VT_AUTO || graphics_mode != KD_TEXT {
return Err(io::Error::other(
"the console VT is already owned by a graphics application",
));
}
let mut termios = unsafe { std::mem::zeroed() };
check(unsafe { libc::tcgetattr(fd, &mut termios) })?;
let mut console = Self {
tty,
termios,
vt_mode,
graphics_mode,
termios_changed: false,
vt_changed: false,
graphics_changed: false,
};
let mut mode = VtMode {
mode: VT_PROCESS,
relsig: libc::SIGUSR1 as i16,
acqsig: libc::SIGUSR2 as i16,
..Default::default()
};
check(unsafe { libc::ioctl(fd, VT_SETMODE, &mut mode) })?;
console.vt_changed = true;
check(unsafe { libc::ioctl(fd, VT_GETSTATE, &mut state) })?;
if u32::from(state.active) != number || stopping() {
return Err(io::Error::other("console ownership changed during startup"));
}
check(unsafe { libc::ioctl(fd, KDSETMODE, KD_GRAPHICS) })?;
console.graphics_changed = true;
let raw = input_mode(termios);
check(unsafe { libc::tcsetattr(fd, libc::TCSANOW, &raw) })?;
console.termios_changed = true;
Ok(console)
}
}
fn terminal_name(fd: libc::c_int) -> String {
fn name(fd: libc::c_int) -> Option<String> {
let mut path = [0 as libc::c_char; 256];
if unsafe { libc::ttyname_r(fd, path.as_mut_ptr(), path.len()) } != 0 {
return None;
}
Some(
unsafe { std::ffi::CStr::from_ptr(path.as_ptr()) }
.to_string_lossy()
.into_owned(),
)
}
match name(fd) {
Some(path) if path != "/dev/tty" => path,
_ => name(libc::STDIN_FILENO).unwrap_or_else(|| "/dev/tty".into()),
}
}
fn console_path(path: &str) -> bool {
path.strip_prefix("/dev/tty").is_some_and(|number| {
!number.is_empty()
&& number.bytes().all(|b| b.is_ascii_digit())
&& number.parse::<u32>().is_ok_and(|n| (1..=63).contains(&n))
})
}
impl Drop for VirtualTerminal {
fn drop(&mut self) {
let fd = self.tty.as_raw_fd();
if self.graphics_changed {
report("restore console graphics mode", unsafe {
libc::ioctl(fd, KDSETMODE, self.graphics_mode)
});
}
if self.termios_changed {
unsafe { libc::tcflush(fd, libc::TCIFLUSH) };
report("restore terminal input", unsafe {
libc::tcsetattr(fd, libc::TCSANOW, &self.termios)
});
}
if self.vt_changed {
if ACQUIRE.swap(false, Ordering::Relaxed) {
report("acknowledge VT acquire", unsafe { libc::ioctl(fd, VT_RELDISP, VT_ACKACQ) });
}
if RELEASE.swap(false, Ordering::Relaxed) {
report("acknowledge VT release", unsafe { libc::ioctl(fd, VT_RELDISP, 1) });
}
report("restore VT switching", unsafe { libc::ioctl(fd, VT_SETMODE, &self.vt_mode) });
}
}
}
fn check(result: libc::c_int) -> io::Result<()> {
if result < 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
fn report(operation: &str, result: libc::c_int) {
if let Err(error) = check(result) {
eprintln!("{operation}: {error}");
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::fd::FromRawFd;
use std::os::unix::process::CommandExt;
use std::process::{Command, Stdio};
fn open_pty() -> (File, File) {
let (mut master, mut slave) = (-1, -1);
check(unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null(),
std::ptr::null(),
)
})
.unwrap();
unsafe { (File::from_raw_fd(master), File::from_raw_fd(slave)) }
}
#[test]
fn vt_abi_layout() {
assert_eq!(std::mem::size_of::<VtMode>(), 8);
assert_eq!(std::mem::size_of::<VtState>(), 6);
}
#[test]
fn pseudo_terminal_startup_restores_input_and_signal_handlers() {
const CHILD: &str = "MEDIAS_PTY_TEST_CHILD";
if std::env::var_os(CHILD).is_none() {
let (_master, tty) = open_pty();
let fd = tty.as_raw_fd();
let mut before: libc::termios = unsafe { std::mem::zeroed() };
check(unsafe { libc::tcgetattr(fd, &mut before) }).unwrap();
let mut command = Command::new(std::env::current_exe().unwrap());
command
.args([
"--exact",
"console::tests::pseudo_terminal_startup_restores_input_and_signal_handlers",
])
.env(CHILD, "1")
.env_remove("SUDO_TTY")
.stdin(Stdio::from(tty.try_clone().unwrap()));
unsafe {
command.pre_exec(|| {
check(libc::setsid())?;
check(libc::ioctl(libc::STDIN_FILENO, libc::TIOCSCTTY, 0))
});
}
assert!(command.status().unwrap().success());
let mut after: libc::termios = unsafe { std::mem::zeroed() };
check(unsafe { libc::tcgetattr(fd, &mut after) }).unwrap();
assert_eq!(after.c_iflag, before.c_iflag);
assert_eq!(after.c_oflag, before.c_oflag);
assert_eq!(after.c_cflag, before.c_cflag);
assert_eq!(after.c_lflag, before.c_lflag);
assert_eq!(after.c_cc, before.c_cc);
return;
}
let background = std::env::var(CHILD).unwrap() == "background";
if background {
assert!(!terminal_is_foreground(libc::STDIN_FILENO));
unsafe { libc::signal(libc::SIGTTOU, libc::SIG_IGN) };
}
let mut before: libc::sigaction = unsafe { std::mem::zeroed() };
check(unsafe { libc::sigaction(libc::SIGINT, std::ptr::null(), &mut before) }).unwrap();
let console = Console::acquire().unwrap();
assert!(!console.owns_vt());
if background {
assert!(console.terminal_input.is_none());
} else {
let input = console.terminal_input.as_ref().unwrap();
let mut mode: libc::termios = unsafe { std::mem::zeroed() };
check(unsafe { libc::tcgetattr(input.tty.as_raw_fd(), &mut mode) }).unwrap();
assert_eq!(mode.c_lflag & (libc::ICANON | libc::ECHO), 0);
assert_ne!(mode.c_lflag & libc::ISIG, 0);
assert_eq!(mode.c_iflag & (libc::IXON | libc::IXOFF), 0);
}
check(unsafe { libc::raise(libc::SIGINT) }).unwrap();
assert!(stopping());
drop(console);
let mut after: libc::sigaction = unsafe { std::mem::zeroed() };
check(unsafe { libc::sigaction(libc::SIGINT, std::ptr::null(), &mut after) }).unwrap();
assert_eq!(after.sa_sigaction, before.sa_sigaction);
if !background {
let status = Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"console::tests::pseudo_terminal_startup_restores_input_and_signal_handlers",
])
.env(CHILD, "background")
.process_group(0)
.status()
.unwrap();
assert!(status.success());
}
}
#[test]
fn startup_without_controlling_terminal_keeps_shutdown_handlers() {
const CHILD: &str = "MEDIAS_DETACHED_TEST_CHILD";
if std::env::var_os(CHILD).is_none() {
let mut command = Command::new(std::env::current_exe().unwrap());
command
.args([
"--exact",
"console::tests::startup_without_controlling_terminal_keeps_shutdown_handlers",
])
.env(CHILD, "1")
.env_remove("SUDO_TTY")
.stdin(Stdio::null());
unsafe { command.pre_exec(|| check(libc::setsid())) };
assert!(command.status().unwrap().success());
return;
}
let console = Console::acquire().unwrap();
assert!(!console.owns_vt());
assert!(console.terminal_input.is_none());
check(unsafe { libc::raise(libc::SIGTERM) }).unwrap();
assert!(stopping());
}
#[test]
fn only_numbered_linux_consoles_use_vt_ownership() {
for number in 1..=63 {
assert_eq!(vt_number(libc::makedev(4, number)), Some(number));
}
for (major, minor) in [(4, 0), (4, 64), (5, 0), (136, 8), (137, 8), (188, 0)] {
assert_eq!(vt_number(libc::makedev(major, minor)), None);
}
}
#[test]
fn non_terminal_ioctl_errors_are_not_bypassed() {
let file = OpenOptions::new().read(true).write(true).open("/dev/null").unwrap();
let error = Console::acquire_tty(file, false).err().unwrap();
assert_eq!(error.raw_os_error(), Some(libc::ENOTTY));
}
#[test]
fn proxy_input_mode_preserves_signals_and_restores_terminal() {
let (mut master, mut slave) = (-1, -1);
check(unsafe {
libc::openpty(
&mut master,
&mut slave,
std::ptr::null_mut(),
std::ptr::null(),
std::ptr::null(),
)
})
.unwrap();
let _master = unsafe { File::from_raw_fd(master) };
let tty = unsafe { File::from_raw_fd(slave) };
let mut before: libc::termios = unsafe { std::mem::zeroed() };
check(unsafe { libc::tcgetattr(slave, &mut before) }).unwrap();
let input = TerminalInput::acquire(tty.try_clone().unwrap()).unwrap();
let mut during: libc::termios = unsafe { std::mem::zeroed() };
check(unsafe { libc::tcgetattr(slave, &mut during) }).unwrap();
assert_eq!(during.c_lflag & (libc::ECHO | libc::ICANON), 0);
assert_ne!(during.c_lflag & libc::ISIG, 0);
assert_eq!(during.c_iflag & (libc::IXON | libc::IXOFF), 0);
drop(input);
let mut after: libc::termios = unsafe { std::mem::zeroed() };
check(unsafe { libc::tcgetattr(slave, &mut after) }).unwrap();
assert_eq!(after.c_lflag, before.c_lflag);
assert_eq!(after.c_iflag, before.c_iflag);
assert_eq!(after.c_cc, before.c_cc);
}
#[test]
fn sudo_proxy_requires_a_numbered_console() {
assert!(console_path("/dev/tty1"));
assert!(console_path("/dev/tty63"));
for path in [
"/dev/tty",
"/dev/tty0",
"/dev/tty64",
"/dev/pts/1",
"/dev/tty1/../pts/1",
] {
assert!(!console_path(path));
}
}
#[test]
fn signals_request_cleanup_and_restore_handlers() {
const CHILD: &str = "MEDIAS_SIGNAL_TEST_CHILD";
if std::env::var_os(CHILD).is_none() {
let status = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"console::tests::signals_request_cleanup_and_restore_handlers",
])
.env(CHILD, "1")
.status()
.unwrap();
assert!(status.success());
return;
}
let mut before: libc::sigaction = unsafe { std::mem::zeroed() };
check(unsafe { libc::sigaction(libc::SIGTERM, std::ptr::null(), &mut before) }).unwrap();
let handlers = SignalHandlers::install().unwrap();
for signal in [
libc::SIGINT,
libc::SIGTERM,
libc::SIGHUP,
libc::SIGQUIT,
libc::SIGTSTP,
libc::SIGUSR1,
libc::SIGUSR2,
] {
STOP.store(false, Ordering::Relaxed);
check(unsafe { libc::raise(signal) }).unwrap();
assert!(stopping());
}
assert!(RELEASE.load(Ordering::Relaxed));
assert!(ACQUIRE.load(Ordering::Relaxed));
drop(handlers);
let mut after: libc::sigaction = unsafe { std::mem::zeroed() };
check(unsafe { libc::sigaction(libc::SIGTERM, std::ptr::null(), &mut after) }).unwrap();
assert_eq!(after.sa_sigaction, before.sa_sigaction);
}
}