use super::{AsErrno, Session, Signal as SessionSignal};
use crate::utils::signaling::Signaler;
use calloop::signals::{Signal, Signals};
use nix::{
fcntl::{self, open, OFlag},
libc::c_int,
sys::stat::{dev_t, fstat, major, minor, Mode},
unistd::{close, dup},
Error as NixError, Result as NixResult,
};
use std::{
fmt,
os::unix::io::RawFd,
path::{Path, PathBuf},
str::FromStr,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
#[cfg(feature = "backend_udev")]
use udev::Device as UdevDevice;
use slog::{debug, error, info, o, trace, warn};
pub const SMITHAY_DIRECT_TTY_ENV: &str = "SMITHAY_DIRECT_TTY";
#[allow(dead_code)]
mod tty {
nix::ioctl_read_bad!(kd_get_mode, 0x4B3B, i16);
nix::ioctl_write_int_bad!(kd_set_mode, 0x4B3A);
pub const KD_TEXT: i16 = 0x00;
pub const KD_GRAPHICS: i16 = 0x00;
nix::ioctl_read_bad!(kd_get_kb_mode, 0x4B44, i32);
nix::ioctl_write_int_bad!(kd_set_kb_mode, 0x4B45);
pub const K_RAW: i32 = 0x00;
pub const K_XLATE: i32 = 0x01;
pub const K_MEDIUMRAW: i32 = 0x02;
pub const K_UNICODE: i32 = 0x03;
pub const K_OFF: i32 = 0x04;
nix::ioctl_write_int_bad!(vt_activate, 0x5606);
nix::ioctl_write_int_bad!(vt_wait_active, 0x5607);
nix::ioctl_write_ptr_bad!(vt_set_mode, 0x5602, VtMode);
nix::ioctl_write_int_bad!(vt_rel_disp, 0x5605);
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct VtMode {
pub mode: i8,
pub waitv: i8,
pub relsig: i16,
pub acqsig: i16,
pub frsig: i16,
}
pub const VT_AUTO: i8 = 0x00;
pub const VT_PROCESS: i8 = 0x01;
pub const VT_ACKACQ: i32 = 0x02;
extern "C" {
pub fn __libc_current_sigrtmin() -> i8;
pub fn __libc_current_sigrtmax() -> i8;
}
}
#[cfg(any(target_os = "linux", target_os = "android"))]
const TTY_MAJOR: u64 = 4;
#[cfg(not(any(target_os = "linux", target_os = "android")))]
const TTY_MAJOR: u64 = 0;
#[cfg(not(feature = "backend_udev"))]
fn is_tty_device(dev: dev_t, _path: Option<&Path>) -> bool {
major(dev) == TTY_MAJOR
}
#[cfg(feature = "backend_udev")]
fn is_tty_device(dev: dev_t, path: Option<&Path>) -> bool {
match path {
Some(path) => {
let device = match UdevDevice::from_syspath(path) {
Ok(device) => device,
Err(_) => return major(dev) == TTY_MAJOR || minor(dev) != 0,
};
let res = if let Some(subsystem) = device.subsystem() {
subsystem == "tty"
} else {
major(dev) == TTY_MAJOR
};
res || minor(dev) != 0
}
None => major(dev) == TTY_MAJOR || minor(dev) != 0,
}
}
#[derive(Debug)]
pub struct DirectSession {
tty: RawFd,
active: Arc<AtomicBool>,
vt: i32,
old_keyboard_mode: i32,
logger: ::slog::Logger,
}
pub struct DirectSessionNotifier {
tty: RawFd,
active: Arc<AtomicBool>,
signaler: Signaler<SessionSignal>,
signal: Signal,
logger: ::slog::Logger,
source: Option<Signals>,
}
impl fmt::Debug for DirectSessionNotifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DirectSessionNotifier")
.field("tty", &self.tty)
.field("active", &self.active)
.field("signaler", &self.signaler)
.field("signal", &self.signal)
.field("logger", &self.logger)
.field(
"source",
&match self.source {
Some(_) => "Some(..)",
None => "None",
},
)
.finish()
}
}
impl DirectSession {
pub fn new<L>(tty: Option<&Path>, logger: L) -> Result<(DirectSession, DirectSessionNotifier), Error>
where
L: Into<Option<::slog::Logger>>,
{
let logger = crate::slog_or_fallback(logger)
.new(o!("smithay_module" => "backend_session", "session_type" => "direct/vt"));
let direct_path: PathBuf;
let tty = if tty.is_none() {
if let Ok(direct_tty) = std::env::var(SMITHAY_DIRECT_TTY_ENV) {
info!(
logger,
"{} is set in environment, trying to open {}", SMITHAY_DIRECT_TTY_ENV, &direct_tty
);
let path = PathBuf::from_str(&direct_tty);
match path {
Ok(path) => {
direct_path = path;
Some(direct_path.as_path())
}
Err(err) => {
warn!(
logger,
"Failed to parse {} with value \"{}\": {}",
SMITHAY_DIRECT_TTY_ENV,
direct_tty,
err
);
None
}
}
} else {
None
}
} else {
tty
};
let fd = tty
.map(|path| {
open(
path,
fcntl::OFlag::O_RDWR | fcntl::OFlag::O_CLOEXEC,
Mode::empty(),
)
.map_err(|source| Error::FailedToOpenTTY(String::from(path.to_string_lossy()), source))
})
.unwrap_or_else(|| {
dup(0 ).map_err(|source| Error::FailedToOpenTTY(String::from("<stdin>"), source))
})?;
let active = Arc::new(AtomicBool::new(true));
match DirectSession::setup_tty(tty, fd, logger.clone()) {
Ok((vt, old_keyboard_mode, signal)) => Ok((
DirectSession {
tty: fd,
active: active.clone(),
vt,
old_keyboard_mode,
logger: logger.new(o!("vt" => format!("{}", vt), "component" => "session")),
},
DirectSessionNotifier {
tty: fd,
active,
signaler: Signaler::new(),
signal,
logger: logger.new(o!("vt" => format!("{}", vt), "component" => "session_notifier")),
source: None,
},
)),
Err(err) => {
let _ = close(fd);
Err(err)
}
}
}
fn setup_tty(
path: Option<&Path>,
tty: RawFd,
logger: ::slog::Logger,
) -> Result<(i32, i32, Signal), Error> {
let stat = fstat(tty).map_err(|_| Error::NotRunningFromTTY)?;
if !is_tty_device(stat.st_dev, path) {
return Err(Error::NotRunningFromTTY);
}
let vt_num = minor(stat.st_rdev) as i32;
info!(logger, "Running from tty: {}", vt_num);
let mut mode = 0;
unsafe {
tty::kd_get_mode(tty, &mut mode).map_err(|_| Error::NotRunningFromTTY)?;
}
if mode != tty::KD_TEXT {
return Err(Error::TTYAlreadyInGraphicsMode);
}
unsafe {
tty::vt_activate(tty, vt_num as c_int)
.map_err(|source| Error::FailedToActivateTTY(vt_num, source))?;
tty::vt_wait_active(tty, vt_num as c_int)
.map_err(|source| Error::FailedToWaitForTTY(vt_num, source))?;
}
let mut old_keyboard_mode = 0;
unsafe {
tty::kd_get_kb_mode(tty, &mut old_keyboard_mode)
.map_err(|source| Error::FailedToSaveTTYState(vt_num, source))?;
tty::kd_set_kb_mode(tty, tty::K_OFF)
.map_err(|source| Error::FailedToSetTTYKbMode(vt_num, source))?;
tty::kd_set_mode(tty, tty::KD_GRAPHICS as i32)
.map_err(|source| Error::FailedToSetTTYMode(vt_num, source))?;
}
let signal = ::nix::sys::signal::SIGUSR2;
let mode = tty::VtMode {
mode: tty::VT_PROCESS,
relsig: signal as i16,
acqsig: signal as i16,
..Default::default()
};
unsafe {
tty::vt_set_mode(tty, &mode).map_err(|source| Error::FailedToTakeControlOfTTY(vt_num, source))?;
}
Ok((vt_num, old_keyboard_mode, Signal::SIGUSR2))
}
pub fn vt(&self) -> i32 {
self.vt
}
}
impl Session for DirectSession {
type Error = NixError;
fn open(&mut self, path: &Path, flags: OFlag) -> NixResult<RawFd> {
debug!(self.logger, "Opening device: {:?}", path);
let fd = open(path, flags, Mode::empty())?;
trace!(self.logger, "Fd num: {:?}", fd);
Ok(fd)
}
fn close(&mut self, fd: RawFd) -> NixResult<()> {
debug!(self.logger, "Closing device: {:?}", fd);
close(fd)
}
fn is_active(&self) -> bool {
self.active.load(Ordering::SeqCst)
}
fn seat(&self) -> String {
String::from("seat0")
}
fn change_vt(&mut self, vt_num: i32) -> NixResult<()> {
unsafe { tty::vt_activate(self.tty, vt_num).map(|_| ()) }
}
}
impl AsErrno for NixError {
fn as_errno(&self) -> Option<i32> {
Some(*self as i32)
}
}
impl Drop for DirectSession {
fn drop(&mut self) {
info!(self.logger, "Deallocating tty {}", self.tty);
if let Err(err) = unsafe { tty::kd_set_kb_mode(self.tty, self.old_keyboard_mode) } {
warn!(self.logger, "Unable to restore vt keyboard mode. Error: {}", err);
}
if let Err(err) = unsafe { tty::kd_set_mode(self.tty, tty::KD_TEXT as i32) } {
warn!(self.logger, "Unable to restore vt text mode. Error: {}", err);
}
if let Err(err) = unsafe {
tty::vt_set_mode(
self.tty,
&tty::VtMode {
mode: tty::VT_AUTO,
..Default::default()
},
)
} {
error!(self.logger, "Failed to reset vt handling. Error: {}", err);
}
if let Err(err) = close(self.tty) {
error!(self.logger, "Failed to close tty file descriptor. Error: {}", err);
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub struct Id(usize);
impl DirectSessionNotifier {
fn signal_received(&mut self) {
if self.active.load(Ordering::SeqCst) {
info!(self.logger, "Session shall become inactive.");
self.signaler.signal(SessionSignal::PauseSession);
self.active.store(false, Ordering::SeqCst);
unsafe {
tty::vt_rel_disp(self.tty, 1).expect("Unable to release tty lock");
}
debug!(self.logger, "Session is now inactive");
} else {
debug!(self.logger, "Session will become active again");
unsafe {
tty::vt_rel_disp(self.tty, tty::VT_ACKACQ).expect("Unable to acquire tty lock");
}
self.signaler.signal(SessionSignal::ActivateSession);
self.active.store(true, Ordering::SeqCst);
info!(self.logger, "Session is now active again");
}
}
pub fn signaler(&self) -> Signaler<SessionSignal> {
self.signaler.clone()
}
}
impl calloop::EventSource for DirectSessionNotifier {
type Event = ();
type Metadata = ();
type Ret = ();
fn process_events<F>(
&mut self,
readiness: calloop::Readiness,
token: calloop::Token,
_: F,
) -> std::io::Result<calloop::PostAction>
where
F: FnMut((), &mut ()),
{
let mut source = self.source.take();
if let Some(ref mut source) = source {
source.process_events(readiness, token, |_, _| self.signal_received())?;
}
self.source = source;
Ok(calloop::PostAction::Continue)
}
fn register(
&mut self,
poll: &mut calloop::Poll,
factory: &mut calloop::TokenFactory,
) -> std::io::Result<()> {
if self.source.is_some() {
return Err(std::io::Error::new(
std::io::ErrorKind::AlreadyExists,
"This DirectSessionNotifier is already registered.",
));
}
let mut source = Signals::new(&[self.signal])?;
source.register(poll, factory)?;
self.source = Some(source);
Ok(())
}
fn reregister(
&mut self,
poll: &mut calloop::Poll,
factory: &mut calloop::TokenFactory,
) -> std::io::Result<()> {
if let Some(ref mut source) = self.source {
source.reregister(poll, factory)
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"This DirectSessionNotifier is not currently registered.",
))
}
}
fn unregister(&mut self, poll: &mut calloop::Poll) -> std::io::Result<()> {
if let Some(mut source) = self.source.take() {
source.unregister(poll)
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"This DirectSessionNotifier is not currently registered.",
))
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Failed to open TTY `{0}`")]
FailedToOpenTTY(String, #[source] nix::Error),
#[error("Not running from a TTY")]
NotRunningFromTTY,
#[error("The tty is already in graphics mode, is already a compositor running?")]
TTYAlreadyInGraphicsMode,
#[error("Failed to activate open tty ({0})")]
FailedToActivateTTY(i32, #[source] nix::Error),
#[error("Failed to wait for tty {0} to become active")]
FailedToWaitForTTY(i32, #[source] nix::Error),
#[error("Failed to save old tty ({0}) state")]
FailedToSaveTTYState(i32, #[source] nix::Error),
#[error("Failed to set tty {0} kb mode to K_OFF")]
FailedToSetTTYKbMode(i32, #[source] nix::Error),
#[error("Failed to set tty {0} mode into graphics mode")]
FailedToSetTTYMode(i32, #[source] nix::Error),
#[error("Failed to take control of tty {0}")]
FailedToTakeControlOfTTY(i32, #[source] nix::Error),
}