use std::{io, process::Command};
use signal_hook::consts::*;
use super::{
event::{EventClosure, EventDispatcher, StopReason},
terminate_process, ExitReason,
};
use crate::{
exec::{io_util::was_interrupted, opt_fmt, signal_fmt},
log::{dev_error, dev_info, dev_warn},
system::{
getpgid, getpgrp,
interface::ProcessId,
kill, killpg,
signal::{SignalAction, SignalInfo, SignalNumber},
term::{Terminal, UserTerm},
wait::{Wait, WaitError, WaitOptions},
},
};
pub(crate) fn exec_no_pty(
sudo_pid: ProcessId,
mut command: Command,
) -> io::Result<(ExitReason, Box<dyn FnOnce()>)> {
let mut dispatcher = EventDispatcher::new()?;
let command = command.spawn().map_err(|err| {
dev_error!("Cannot spawn command: {err}");
err
})?;
let command_pid = command.id() as ProcessId;
dev_info!("Executed command with pid {command_pid}");
let mut closure = ExecClosure::new(command_pid, sudo_pid);
let exit_reason = match dispatcher.event_loop(&mut closure) {
StopReason::Break(reason) => match reason {},
StopReason::Exit(reason) => reason,
};
Ok((exit_reason, Box::new(move || drop(dispatcher))))
}
struct ExecClosure {
command_pid: Option<ProcessId>,
sudo_pid: ProcessId,
parent_pgrp: ProcessId,
}
impl ExecClosure {
fn new(command_pid: ProcessId, sudo_pid: ProcessId) -> Self {
Self {
command_pid: Some(command_pid),
sudo_pid,
parent_pgrp: getpgrp(),
}
}
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 handle_sigchld(&mut self, command_pid: ProcessId, dispatcher: &mut EventDispatcher<Self>) {
const OPTS: WaitOptions = WaitOptions::new().all().untraced().no_hang();
let status = loop {
match command_pid.wait(OPTS) {
Err(WaitError::Io(err)) if was_interrupted(&err) => {}
Err(_) => {}
Ok((_pid, status)) => break status,
}
};
if let Some(signal) = status.stop_signal() {
dev_info!(
"command ({command_pid}) was stopped by {}",
signal_fmt(signal),
);
self.suspend_parent(signal, dispatcher);
} else if let Some(signal) = status.term_signal() {
dev_info!(
"command ({command_pid}) was terminated by {}",
signal_fmt(signal),
);
dispatcher.set_exit(ExitReason::Signal(signal));
self.command_pid = None;
} else if let Some(exit_code) = status.exit_status() {
dev_info!("command ({command_pid}) exited with status code {exit_code}");
dispatcher.set_exit(ExitReason::Code(exit_code));
self.command_pid = None;
} else if status.did_continue() {
dev_info!("command ({command_pid}) continued execution");
} else {
dev_warn!("unexpected wait status for command ({command_pid})")
}
}
fn suspend_parent(&self, signal: SignalNumber, dispatcher: &mut EventDispatcher<Self>) {
let mut opt_tty = UserTerm::open().ok();
let mut opt_pgrp = None;
if let Some(tty) = opt_tty.as_ref() {
if let Ok(saved_pgrp) = tty.tcgetpgrp() {
opt_pgrp = Some(saved_pgrp);
} else {
opt_tty.take();
}
}
if let Some(saved_pgrp) = opt_pgrp {
if let SIGTTOU | SIGTTIN = signal {
if saved_pgrp == self.parent_pgrp {
if let Some(command_pgrp) = self.command_pid.and_then(|pid| getpgid(pid).ok()) {
if command_pgrp != self.parent_pgrp
&& opt_tty
.as_ref()
.is_some_and(|tty| tty.tcsetpgrp_nobg(command_pgrp).is_ok())
{
if let Err(err) = killpg(command_pgrp, SIGCONT) {
dev_warn!("cannot send SIGCONT to command ({command_pgrp}): {err}");
}
return;
}
}
}
}
}
if signal == SIGTSTP {
dispatcher.set_signal_action(signal, SignalAction::Default);
}
if let Err(err) = kill(self.sudo_pid, signal) {
dev_warn!(
"cannot send {} to sudo ({}): {err}",
signal_fmt(signal),
self.sudo_pid
);
}
if signal == SIGTSTP {
dispatcher.set_signal_action(signal, SignalAction::Stream);
}
if let Some(saved_pgrp) = opt_pgrp {
if saved_pgrp != self.parent_pgrp {
if let Some(tty) = opt_tty {
tty.tcsetpgrp_nobg(saved_pgrp).ok();
}
}
}
}
}
impl EventClosure for ExecClosure {
type Break = std::convert::Infallible;
type Exit = ExitReason;
fn on_signal(&mut self, info: SignalInfo, dispatcher: &mut EventDispatcher<Self>) {
dev_info!(
"sudo received{} {} from {}",
opt_fmt(info.is_user_signaled(), " user signaled"),
signal_fmt(info.signal()),
info.pid()
);
let Some(command_pid) = self.command_pid else {
dev_info!("command was terminated, ignoring signal");
return;
};
match info.signal() {
SIGCHLD => {
self.handle_sigchld(command_pid, dispatcher);
}
signal => {
if signal == SIGWINCH {
}
if info.is_user_signaled() && self.is_self_terminating(info.pid()) {
return;
}
if signal == SIGALRM {
terminate_process(command_pid, false);
} else {
kill(command_pid, signal).ok();
}
}
}
}
}