use std::io;
use std::os::fd::{AsRawFd, OwnedFd, RawFd};
use std::os::unix::process::CommandExt;
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, MutexGuard};
use rustix::fs::{OFlags, fcntl_getfl};
use rustix::process::{Pid, Signal, getpgrp, getsid, kill_process_group};
use rustix::termios::{
LocalModes, OptionalActions, QueueSelector, Termios, tcflush, tcgetattr, tcgetpgrp, tcgetsid,
tcsetattr, tcsetpgrp,
};
use super::SshError;
static ACTIVE: AtomicBool = AtomicBool::new(false);
#[cfg(test)]
pub(super) static FAIL_RESUME: AtomicBool = AtomicBool::new(false);
#[cfg(test)]
pub(super) static FAIL_FLUSH: AtomicBool = AtomicBool::new(false);
pub struct ForegroundTerminal {
shared: Arc<Shared>,
}
pub struct TerminalEvents {
shared: Arc<Shared>,
}
pub struct StoppedTerminal {
shared: Arc<Shared>,
generation: u64,
signal: i32,
granted: bool,
}
struct Shared {
fd: OwnedFd,
state: Mutex<State>,
}
#[derive(Default)]
struct State {
generation: u64,
active: bool,
receiver_gone: bool,
cancelled: bool,
stop: Option<i32>,
resume: bool,
}
impl Shared {
fn state(&self) -> Result<MutexGuard<'_, State>, SshError> {
self.state.lock().map_err(|_| SshError::Cancelled)
}
}
impl std::fmt::Debug for ForegroundTerminal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ForegroundTerminal").finish_non_exhaustive()
}
}
impl ForegroundTerminal {
pub fn prepare(fd: OwnedFd) -> Result<(Self, TerminalEvents), SshError> {
snapshot(&fd)?;
let shared = Arc::new(Shared {
fd,
state: Mutex::new(State::default()),
});
Ok((
Self {
shared: shared.clone(),
},
TerminalEvents { shared },
))
}
pub(super) fn acquire(&self) -> Result<Lease, SshError> {
let mut state = self.shared.state()?;
if state.receiver_gone || state.cancelled {
return Err(SshError::Cancelled);
}
if state.active || ACTIVE.swap(true, Ordering::AcqRel) {
return Err(SshError::Configuration("terminal already leased"));
}
let (caller, attributes) = match snapshot(&self.shared.fd) {
Ok(value) => value,
Err(error) => {
ACTIVE.store(false, Ordering::Release);
return Err(error);
}
};
state.generation = state.generation.wrapping_add(1);
state.active = true;
state.stop = None;
state.resume = false;
Ok(Lease {
shared: self.shared.clone(),
caller,
attributes,
suspended: None,
restored: false,
})
}
}
impl TerminalEvents {
pub fn try_next_stop(&mut self) -> Result<Option<StoppedTerminal>, SshError> {
let mut state = self.shared.state()?;
if state.cancelled {
return Err(SshError::Cancelled);
}
Ok(state.stop.take().map(|signal| StoppedTerminal {
shared: self.shared.clone(),
generation: state.generation,
signal,
granted: false,
}))
}
}
impl Drop for TerminalEvents {
fn drop(&mut self) {
if let Ok(mut state) = self.shared.state.lock() {
state.receiver_gone = true;
state.cancelled = true;
}
}
}
impl StoppedTerminal {
pub fn signal(&self) -> i32 {
self.signal
}
pub fn resume(mut self) -> Result<(), SshError> {
let mut state = self.shared.state()?;
if !state.active || state.generation != self.generation || state.cancelled {
return Err(SshError::Cancelled);
}
snapshot(&self.shared.fd)?;
state.resume = true;
self.granted = true;
Ok(())
}
}
impl Drop for StoppedTerminal {
fn drop(&mut self) {
if !self.granted
&& let Ok(mut state) = self.shared.state.lock()
&& state.active
&& state.generation == self.generation
{
state.cancelled = true;
}
}
}
pub(super) struct Lease {
shared: Arc<Shared>,
caller: Pid,
attributes: Termios,
suspended: Option<Termios>,
restored: bool,
}
impl Lease {
pub(super) fn prepare_command(&self, command: &mut Command) -> Result<(), SshError> {
let fd = self.shared.fd.as_raw_fd();
let blocked = ttou_set();
unsafe {
command.pre_exec(move || child_handoff(fd, &blocked));
}
Ok(())
}
pub(super) fn check(&mut self, pid: Pid) -> Result<bool, SshError> {
let resume = {
let mut state = self.shared.state()?;
if state.cancelled || state.receiver_gone {
return Err(SshError::Cancelled);
}
std::mem::take(&mut state.resume)
};
if resume {
snapshot(&self.shared.fd)?;
let attributes = self.suspended.as_ref().ok_or(SshError::Cancelled)?;
self.restored = false;
with_sigttou_blocked(|| {
tcsetattr(&self.shared.fd, OptionalActions::Now, attributes)?;
#[cfg(test)]
if FAIL_RESUME.swap(false, Ordering::Relaxed) {
return Err(rustix::io::Errno::IO);
}
tcsetpgrp(&self.shared.fd, pid)
})?;
kill_process_group(pid, Signal::CONT).map_err(io::Error::from)?;
self.suspended = None;
}
Ok(self.suspended.is_some())
}
pub(super) fn stopped(&mut self, pid: Pid, signal: i32) -> Result<(), SshError> {
kill_process_group(pid, Signal::STOP).map_err(io::Error::from)?;
if self.suspended.is_some() {
return Ok(());
}
self.suspended = Some(tcgetattr(&self.shared.fd).map_err(io::Error::from)?);
self.restore(false)?;
let mut state = self.shared.state()?;
state.stop = Some(signal);
Ok(())
}
pub(super) fn continued(&self, pid: Pid) -> Result<(), SshError> {
if self.suspended.is_some() {
kill_process_group(pid, Signal::STOP).map_err(io::Error::from)?;
}
Ok(())
}
pub(super) fn restore(&mut self, completed: bool) -> Result<(), SshError> {
if !self.restored {
let mut returned = false;
let result = with_sigttou_blocked(|| {
let ordinary_input = tcgetattr(&self.shared.fd).is_ok_and(|attributes| {
attributes.local_modes.contains(LocalModes::ECHO)
&& attributes.input_modes == self.attributes.input_modes
&& attributes.local_modes == self.attributes.local_modes
});
let input = if completed && ordinary_input {
Ok(())
} else {
tcflush(&self.shared.fd, QueueSelector::IFlush)
};
#[cfg(test)]
let input = if FAIL_FLUSH.swap(false, Ordering::Relaxed) {
Err(rustix::io::Errno::IO)
} else {
input
};
let attributes = tcsetattr(&self.shared.fd, OptionalActions::Now, &self.attributes);
let foreground = tcsetpgrp(&self.shared.fd, self.caller);
returned = foreground.is_ok();
input.and(attributes).and(foreground)
});
self.restored = returned;
result?;
}
Ok(())
}
}
impl Drop for Lease {
fn drop(&mut self) {
let _ = self.restore(false);
if let Ok(mut state) = self.shared.state.lock() {
state.active = false;
state.stop = None;
state.resume = false;
state.cancelled = state.receiver_gone;
}
ACTIVE.store(false, Ordering::Release);
}
}
fn snapshot(fd: &OwnedFd) -> Result<(Pid, Termios), SshError> {
if (fcntl_getfl(fd).map_err(io::Error::from)? & OFlags::ACCMODE) != OFlags::RDWR {
return Err(SshError::Configuration(
"terminal descriptor must be read/write",
));
}
let caller = getpgrp();
let session = getsid(None).map_err(io::Error::from)?;
if tcgetsid(fd).map_err(io::Error::from)? != session
|| tcgetpgrp(fd).map_err(io::Error::from)? != caller
{
return Err(SshError::Configuration(
"caller does not own foreground terminal",
));
}
let attributes = tcgetattr(fd).map_err(io::Error::from)?;
if attributes.local_modes.contains(LocalModes::TOSTOP) {
return Err(SshError::Configuration("terminal TOSTOP is unsupported"));
}
Ok((caller, attributes))
}
fn ttou_set() -> libc::sigset_t {
unsafe {
let mut set = std::mem::zeroed();
libc::sigemptyset(&mut set);
libc::sigaddset(&mut set, libc::SIGTTOU);
set
}
}
fn child_handoff(fd: RawFd, blocked: &libc::sigset_t) -> io::Result<()> {
unsafe {
if libc::setpgid(0, 0) != 0 {
return Err(io::Error::last_os_error());
}
let mut previous = std::mem::zeroed();
let error = libc::pthread_sigmask(libc::SIG_BLOCK, blocked, &mut previous);
if error != 0 {
return Err(io::Error::from_raw_os_error(error));
}
let result = libc::tcsetpgrp(fd, libc::getpgrp());
let error = (result != 0).then(io::Error::last_os_error);
let restored = libc::pthread_sigmask(libc::SIG_SETMASK, &previous, std::ptr::null_mut());
if let Some(error) = error {
Err(error)
} else if restored != 0 {
Err(io::Error::from_raw_os_error(restored))
} else {
Ok(())
}
}
}
pub(super) fn with_sigttou_blocked(
operation: impl FnOnce() -> rustix::io::Result<()>,
) -> io::Result<()> {
let blocked = ttou_set();
unsafe {
let mut previous = std::mem::zeroed();
let error = libc::pthread_sigmask(libc::SIG_BLOCK, &blocked, &mut previous);
if error != 0 {
return Err(io::Error::from_raw_os_error(error));
}
let result = operation().map_err(io::Error::from);
let restored = libc::pthread_sigmask(libc::SIG_SETMASK, &previous, std::ptr::null_mut());
if restored != 0 {
return result.and(Err(io::Error::from_raw_os_error(restored)));
}
result
}
}