use std::collections::VecDeque;
use std::ffi::c_int;
use std::io;
use std::process::{exit, Command};
use signal_hook::consts::*;
use crate::exec::event::{EventClosure, EventDispatcher, StopReason};
use crate::exec::use_pty::monitor::exec_monitor;
use crate::exec::use_pty::SIGCONT_FG;
use crate::exec::{cond_fmt, opt_fmt, signal_fmt, terminate_process};
use crate::exec::{
io_util::{retry_while_interrupted, was_interrupted},
use_pty::backchannel::{BackchannelPair, MonitorMessage, ParentBackchannel, ParentMessage},
ExitReason,
};
use crate::log::{dev_error, dev_info, dev_warn};
use crate::system::signal::{SignalAction, SignalHandler, SignalNumber};
use crate::system::term::{Pty, PtyLeader, Terminal, UserTerm};
use crate::system::wait::{Wait, WaitError, WaitOptions};
use crate::system::{chown, fork, getpgrp, kill, killpg, ForkResult, Group, User};
use crate::system::{getpgid, interface::ProcessId, signal::SignalInfo};
use super::pipe::Pipe;
use super::SIGCONT_BG;
pub(crate) fn exec_pty(
sudo_pid: ProcessId,
mut command: Command,
user_tty: UserTerm,
) -> io::Result<(ExitReason, Box<dyn FnOnce()>)> {
let pty = get_pty()?;
let mut backchannels = BackchannelPair::new().map_err(|err| {
dev_error!("unable to create backchannel: {err}");
err
})?;
if let Err(err) = SignalHandler::with_action(SIGTTIN, SignalAction::Ignore) {
dev_error!("unable to set handler for SIGTTIN: {err}");
}
if let Err(err) = SignalHandler::with_action(SIGTTOU, SignalAction::Ignore) {
dev_error!("unable to set handler for SIGTTOU: {err}");
}
let parent_pgrp = getpgrp();
let clone_follower = || {
pty.follower.try_clone().map_err(|err| {
dev_error!("cannot clone pty follower: {err}");
err
})
};
command.stdin(clone_follower()?);
command.stdout(clone_follower()?);
command.stderr(clone_follower()?);
let mut dispatcher = EventDispatcher::<ParentClosure>::new()?;
let mut tty_pipe = Pipe::new(user_tty, pty.leader);
let (user_tty, pty_leader) = tty_pipe.both_mut();
dispatcher.set_read_callback(user_tty, |parent, _| {
parent.tty_pipe.read_left().ok();
});
dispatcher.set_write_callback(pty_leader, |parent, _| {
parent.tty_pipe.write_right().ok();
});
dispatcher.set_read_callback(pty_leader, |parent, _| {
parent.tty_pipe.read_right().ok();
});
dispatcher.set_write_callback(user_tty, |parent, _| {
parent.tty_pipe.write_left().ok();
});
let mut foreground = user_tty
.tcgetpgrp()
.is_ok_and(|tty_pgrp| tty_pgrp == parent_pgrp);
dev_info!(
"sudo is runnning in the {}",
cond_fmt(foreground, "foreground", "background")
);
let pipeline = false;
let exec_bg = false;
let mut term_raw = false;
if let Err(err) = user_tty.copy_to(&pty.follower) {
dev_error!("cannot copy terminal settings to pty: {err}");
foreground = false;
}
if foreground && !pipeline && !exec_bg && user_tty.set_raw_mode(false).is_ok() {
term_raw = true;
}
let ForkResult::Parent(monitor_pid) = fork().map_err(|err| {
dev_error!("unable to fork monitor process: {err}");
err
})? else {
drop(tty_pipe);
drop(backchannels.parent);
dispatcher.unregister_handlers();
if let Err(err) = exec_monitor(
pty.follower,
command,
foreground && !pipeline && !exec_bg,
&mut backchannels.monitor,
) {
match err.try_into() {
Ok(msg) => {
if let Err(err) = backchannels.monitor.send(&msg) {
dev_error!("unable to send status to parent: {err}");
}
}
Err(err) => dev_warn!("execution error {err:?} cannot be send over backchannel"),
}
}
exit(1)
};
drop(pty.follower);
drop(backchannels.monitor);
retry_while_interrupted(|| backchannels.parent.send(&MonitorMessage::ExecCommand)).map_err(
|err| {
dev_error!("unable to send green light to monitor: {err}");
err
},
)?;
let mut closure = ParentClosure::new(
monitor_pid,
sudo_pid,
parent_pgrp,
backchannels.parent,
tty_pipe,
foreground,
term_raw,
&mut dispatcher,
);
let exit_reason = closure.run(&mut dispatcher);
closure.tty_pipe.flush_left().ok();
if closure.term_raw {
if let Ok(pgrp) = closure.tty_pipe.left().tcgetpgrp() {
if pgrp == closure.parent_pgrp {
match closure.tty_pipe.left_mut().restore(false) {
Ok(()) => closure.term_raw = false,
Err(err) => dev_warn!("cannot restore terminal settings: {err}"),
}
}
}
}
match exit_reason {
Ok(exit_reason) => Ok((exit_reason, Box::new(move || drop(dispatcher)))),
Err(err) => Err(err),
}
}
fn get_pty() -> io::Result<Pty> {
let tty_gid = Group::from_name("tty")
.unwrap_or(None)
.map(|group| group.gid);
let pty = Pty::open().map_err(|err| {
dev_error!("unable to allocate pty: {err}");
err
})?;
chown(&pty.path, User::effective_uid(), tty_gid).map_err(|err| {
dev_error!("unable to change owner for pty: {err}");
err
})?;
Ok(pty)
}
struct ParentClosure {
monitor_pid: Option<ProcessId>,
sudo_pid: ProcessId,
parent_pgrp: ProcessId,
command_pid: Option<ProcessId>,
backchannel: ParentBackchannel,
tty_pipe: Pipe<UserTerm, PtyLeader>,
foreground: bool,
term_raw: bool,
message_queue: VecDeque<MonitorMessage>,
}
impl ParentClosure {
#[allow(clippy::too_many_arguments)]
fn new(
monitor_pid: ProcessId,
sudo_pid: ProcessId,
parent_pgrp: ProcessId,
backchannel: ParentBackchannel,
tty_pipe: Pipe<UserTerm, PtyLeader>,
foreground: bool,
term_raw: bool,
dispatcher: &mut EventDispatcher<Self>,
) -> Self {
dispatcher.set_read_callback(&backchannel, |parent, dispatcher| {
parent.on_message_received(dispatcher)
});
dispatcher.set_write_callback(&backchannel, |parent, dispatcher| {
parent.check_message_queue(dispatcher)
});
Self {
monitor_pid: Some(monitor_pid),
sudo_pid,
parent_pgrp,
command_pid: None,
backchannel,
tty_pipe,
foreground,
term_raw,
message_queue: VecDeque::new(),
}
}
fn run(&mut self, dispatcher: &mut EventDispatcher<Self>) -> io::Result<ExitReason> {
match dispatcher.event_loop(self) {
StopReason::Break(err) | StopReason::Exit(ParentExit::Backchannel(err)) => Err(err),
StopReason::Exit(ParentExit::Command(exit_reason)) => Ok(exit_reason),
}
}
fn on_message_received(&mut self, dispatcher: &mut EventDispatcher<Self>) {
match self.backchannel.recv() {
Err(err) if was_interrupted(&err) => {}
Err(err) => {
if err.kind() == io::ErrorKind::UnexpectedEof {
dev_info!("parent received EOF from backchannel");
dispatcher.set_exit(err.into());
} else {
dev_error!("could not receive message from monitor: {err}");
if !dispatcher.got_break() {
dispatcher.set_break(err);
}
}
}
Ok(event) => {
match event {
ParentMessage::CommandPid(pid) => {
dev_info!("received command PID ({pid}) from monitor");
self.command_pid = pid.into();
}
ParentMessage::CommandStatus(status) => {
if let Some(exit_code) = status.exit_status() {
dev_info!("command exited with status code {exit_code}");
dispatcher.set_exit(ExitReason::Code(exit_code).into());
} else if let Some(signal) = status.term_signal() {
dev_info!("command was terminated by {}", signal_fmt(signal));
dispatcher.set_exit(ExitReason::Signal(signal).into());
} else if let Some(signal) = status.stop_signal() {
dev_info!(
"command was stopped by {}, suspending parent",
signal_fmt(signal)
);
if let Some(signal) = self.suspend_pty(signal, dispatcher) {
self.schedule_signal(signal);
}
}
}
ParentMessage::IoError(code) => {
let err = io::Error::from_raw_os_error(code);
dev_info!("received error ({code}) for monitor: {err}");
dispatcher.set_break(err);
}
ParentMessage::ShortRead => {
dev_info!("received short read error for monitor");
dispatcher.set_break(io::ErrorKind::UnexpectedEof.into());
}
}
}
}
}
fn is_self_terminating(&self, signaler_pid: ProcessId) -> bool {
if signaler_pid != 0 {
if Some(signaler_pid) == self.command_pid {
return true;
}
if let Ok(signaler_pgrp) = getpgid(signaler_pid) {
if Some(signaler_pgrp) == self.command_pid || signaler_pgrp == self.sudo_pid {
return true;
}
}
}
false
}
fn schedule_signal(&mut self, signal: c_int) {
dev_info!("scheduling message with {} for monitor", signal_fmt(signal));
self.message_queue.push_back(MonitorMessage::Signal(signal));
}
fn check_message_queue(&mut self, dispatcher: &mut EventDispatcher<Self>) {
if let Some(msg) = self.message_queue.front() {
dev_info!("sending message {msg:?} to monitor over backchannel");
match self.backchannel.send(msg) {
Ok(()) => {
self.message_queue.pop_front().unwrap();
}
Err(err) if err.kind() == io::ErrorKind::BrokenPipe => {
dev_error!("broken pipe while writing to monitor over backchannel");
dispatcher.set_break(err);
}
Err(_) => {}
}
}
}
fn handle_sigchld(&mut self, monitor_pid: ProcessId, dispatcher: &mut EventDispatcher<Self>) {
const OPTS: WaitOptions = WaitOptions::new().all().untraced().no_hang();
let status = loop {
match monitor_pid.wait(OPTS) {
Err(WaitError::Io(err)) if was_interrupted(&err) => {}
Err(WaitError::Io(_err)) => dev_info!("parent could not wait for monitor: {_err}"),
Err(WaitError::NotReady) => dev_info!("monitor process without status update"),
Ok((_pid, status)) => break status,
}
};
if let Some(_code) = status.exit_status() {
dev_info!("monitor ({monitor_pid}) exited with status code {_code}");
self.monitor_pid = None;
} else if let Some(_signal) = status.term_signal() {
dev_info!(
"monitor ({monitor_pid}) was terminated by {}",
signal_fmt(_signal)
);
self.monitor_pid = None;
} else if let Some(signal) = status.stop_signal() {
dev_info!(
"monitor ({monitor_pid}) was stopped by {}, suspending sudo",
signal_fmt(signal)
);
if let Some(signal) = self.suspend_pty(signal, dispatcher) {
self.schedule_signal(signal);
}
} else if status.did_continue() {
dev_info!("monitor ({monitor_pid}) continued execution");
} else {
dev_warn!("unexpected wait status for monitor ({monitor_pid})")
}
}
fn suspend_pty(
&mut self,
signal: SignalNumber,
dispatcher: &mut EventDispatcher<Self>,
) -> Option<SignalNumber> {
dispatcher.set_signal_action(SIGCONT, SignalAction::Ignore);
if let SIGTTOU | SIGTTIN = signal {
if !self.foreground && self.check_foreground().is_err() {
return None;
}
if self.foreground {
dev_info!(
"command received {}, parent running in the foreground",
signal_fmt(signal)
);
if !self.term_raw {
if self.tty_pipe.left_mut().set_raw_mode(false).is_ok() {
self.term_raw = true;
}
return Some(SIGCONT_FG);
}
}
}
if self.term_raw {
match self.tty_pipe.left_mut().restore(false) {
Ok(()) => self.term_raw = false,
Err(err) => dev_warn!("unable to restore terminal settings: {err}"),
}
}
if signal != SIGSTOP {
dispatcher.set_signal_action(signal, SignalAction::Default);
}
if self.parent_pgrp != self.sudo_pid && kill(self.parent_pgrp, 0).is_err()
|| killpg(self.parent_pgrp, signal).is_err()
{
dev_error!("no parent to suspend, terminating command");
if let Some(command_pid) = self.command_pid.take() {
terminate_process(command_pid, true);
}
}
if signal != SIGSTOP {
dispatcher.set_signal_action(signal, SignalAction::Stream);
}
if self.command_pid.is_none() || self.resume_terminal().is_err() {
return None;
}
let ret_signal = if self.term_raw {
SIGCONT_FG
} else {
SIGCONT_BG
};
dispatcher.set_signal_action(SIGCONT, SignalAction::Stream);
Some(ret_signal)
}
fn check_foreground(&mut self) -> io::Result<()> {
let pgrp = self.tty_pipe.left().tcgetpgrp()?;
self.foreground = pgrp == self.parent_pgrp;
Ok(())
}
fn resume_terminal(&mut self) -> io::Result<()> {
self.check_foreground()?;
self.tty_pipe
.left()
.copy_to(self.tty_pipe.right())
.map_err(|err| {
dev_error!("cannot copy terminal settings to pty: {err}");
err
})?;
dev_info!(
"parent is in {} ({} -> {})",
cond_fmt(self.foreground, "foreground", "background"),
cond_fmt(self.term_raw, "raw", "cooked"),
cond_fmt(self.foreground, "raw", "cooked"),
);
if self.foreground {
if self.tty_pipe.left_mut().set_raw_mode(false).is_ok() {
self.term_raw = true;
}
} else {
self.term_raw = false;
}
Ok(())
}
}
enum ParentExit {
Backchannel(io::Error),
Command(ExitReason),
}
impl From<io::Error> for ParentExit {
fn from(err: io::Error) -> Self {
Self::Backchannel(err)
}
}
impl From<ExitReason> for ParentExit {
fn from(reason: ExitReason) -> Self {
Self::Command(reason)
}
}
impl EventClosure for ParentClosure {
type Break = io::Error;
type Exit = ParentExit;
fn on_signal(&mut self, info: SignalInfo, dispatcher: &mut EventDispatcher<Self>) {
dev_info!(
"parent received{} {} from {}",
opt_fmt(info.is_user_signaled(), " user signaled"),
signal_fmt(info.signal()),
info.pid()
);
let Some(monitor_pid) = self.monitor_pid else {
dev_info!("monitor was terminated, ignoring signal");
return;
};
match info.signal() {
SIGCHLD => self.handle_sigchld(monitor_pid, dispatcher),
SIGCONT => {
self.resume_terminal().ok();
}
SIGWINCH => {}
_ if info.is_user_signaled() && self.is_self_terminating(info.pid()) => {}
signal => self.schedule_signal(signal),
}
}
}