mod event;
mod interface;
mod io_util;
mod no_pty;
mod use_pty;
use std::{
borrow::Cow,
env,
ffi::{c_int, OsStr},
io,
os::unix::ffi::OsStrExt,
os::unix::process::CommandExt,
process::Command,
time::Duration,
};
use crate::{
common::Environment,
log::dev_warn,
system::{
_exit,
interface::ProcessId,
killpg,
signal::{consts::*, signal_name},
wait::{Wait, WaitError, WaitOptions},
},
};
use crate::{
exec::no_pty::exec_no_pty,
log::dev_info,
system::{set_target_user, signal::SignalNumber, term::UserTerm},
};
use crate::{log::user_error, system::kill};
pub use interface::RunOptions;
use self::{
event::{EventRegistry, Process},
io_util::was_interrupted,
use_pty::{exec_pty, SIGCONT_BG, SIGCONT_FG},
};
pub fn run_command(options: &impl RunOptions, env: Environment) -> io::Result<ExecOutput> {
match run_command_internal(options, env)? {
ProcessOutput::SudoExit { output } => Ok(output),
ProcessOutput::ChildExit => _exit(1),
}
}
fn run_command_internal(options: &impl RunOptions, env: Environment) -> io::Result<ProcessOutput> {
let qualified_path = options.command()?;
let mut command = Command::new(qualified_path);
command.args(options.arguments()).env_clear().envs(env);
if let Some(arg0) = options.arg0() {
command.arg0(arg0);
}
if options.is_login() {
let mut process_name = qualified_path
.file_name()
.map(|osstr| osstr.as_bytes().to_vec())
.unwrap_or_else(Vec::new);
process_name.insert(0, b'-');
command.arg0(OsStr::from_bytes(&process_name));
}
let path = options
.chdir()
.cloned()
.or_else(|| options.is_login().then(|| options.user().home.clone()));
set_target_user(
&mut command,
options.user().clone(),
options.group().clone(),
);
if let Some(path) = path {
let is_chdir = options.chdir().is_some();
unsafe {
command.pre_exec(move || {
if let Err(err) = env::set_current_dir(&path) {
user_error!("unable to change directory to {}: {}", path.display(), err);
if is_chdir {
return Err(err);
}
}
Ok(())
});
}
}
if options.use_pty() {
match UserTerm::open() {
Ok(user_tty) => exec_pty(options.pid(), command, user_tty),
Err(err) => {
dev_info!("Could not open user's terminal, not allocating a pty: {err}");
exec_no_pty(options.pid(), command)
}
}
} else {
exec_no_pty(options.pid(), command)
}
}
pub struct ExecOutput {
pub command_exit_reason: ExitReason,
pub restore_signal_handlers: Box<dyn FnOnce()>,
}
enum ProcessOutput {
SudoExit { output: ExecOutput },
ChildExit,
}
#[derive(Debug)]
pub enum ExitReason {
Code(i32),
Signal(i32),
}
fn terminate_process(pid: ProcessId, use_killpg: bool) {
let kill_fn = if use_killpg { killpg } else { kill };
kill_fn(pid, SIGHUP).ok();
kill_fn(pid, SIGTERM).ok();
std::thread::sleep(Duration::from_secs(2));
kill_fn(pid, SIGKILL).ok();
}
trait HandleSigchld: Process {
const OPTIONS: WaitOptions;
fn on_exit(&mut self, exit_code: c_int, registry: &mut EventRegistry<Self>);
fn on_term(&mut self, signal: SignalNumber, registry: &mut EventRegistry<Self>);
fn on_stop(&mut self, signal: SignalNumber, registry: &mut EventRegistry<Self>);
}
fn handle_sigchld<T: HandleSigchld>(
handler: &mut T,
registry: &mut EventRegistry<T>,
child_name: &'static str,
child_pid: ProcessId,
) {
let status = loop {
match child_pid.wait(T::OPTIONS) {
Err(WaitError::Io(err)) if was_interrupted(&err) => {}
Err(WaitError::Io(err)) => {
return dev_info!("cannot wait for {child_pid} ({child_name}): {err}");
}
Err(WaitError::NotReady) => {
return dev_info!("{child_pid} ({child_name}) has no status report");
}
Ok((_pid, status)) => break status,
}
};
if let Some(exit_code) = status.exit_status() {
dev_info!("{child_pid} ({child_name}) exited with status code {exit_code}");
handler.on_exit(exit_code, registry)
} else if let Some(signal) = status.stop_signal() {
dev_info!(
"{child_pid} ({child_name}) was stopped by {}",
signal_fmt(signal),
);
handler.on_stop(signal, registry)
} else if let Some(signal) = status.term_signal() {
dev_info!(
"{child_pid} ({child_name}) was terminated by {}",
signal_fmt(signal),
);
handler.on_term(signal, registry)
} else if status.did_continue() {
dev_info!("{child_pid} ({child_name}) continued execution");
} else {
dev_warn!("unexpected wait status for {child_pid} ({child_name})")
}
}
fn signal_fmt(signal: SignalNumber) -> Cow<'static, str> {
match signal_name(signal) {
name @ Cow::Owned(_) => match signal {
SIGCONT_BG => "SIGCONT_BG".into(),
SIGCONT_FG => "SIGCONT_FG".into(),
_ => name,
},
name => name,
}
}
const fn cond_fmt<'a>(cond: bool, true_s: &'a str, false_s: &'a str) -> &'a str {
if cond {
true_s
} else {
false_s
}
}
const fn opt_fmt(cond: bool, s: &str) -> &str {
cond_fmt(cond, s, "")
}