use anyhow::{Context, Result};
use nix::sys::signal::{self, Signal};
use nix::sys::wait::{waitpid, WaitStatus};
use nix::unistd::{fork, ForkResult, Pid};
use std::process::Command;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use crate::config::ShimConfig;
use crate::platform::PlatformExecutorTrait;
pub struct UnixExecutor {
signal_setup: Arc<AtomicBool>,
}
impl UnixExecutor {
pub fn new() -> Self {
Self {
signal_setup: Arc::new(AtomicBool::new(false)),
}
}
fn setup_signal_handling(&self, config: &ShimConfig) -> Result<()> {
if self.signal_setup.load(Ordering::Relaxed) {
return Ok(());
}
let signal_config = config.signal_handling.as_ref();
let ignore_sigint = signal_config.and_then(|s| s.ignore_sigint).unwrap_or(true);
if ignore_sigint {
unsafe {
signal::signal(Signal::SIGINT, signal::SigHandler::SigIgn)
.context("Failed to set SIGINT handler")?;
}
}
unsafe {
signal::signal(Signal::SIGTERM, signal::SigHandler::SigIgn)
.context("Failed to set SIGTERM handler")?;
}
self.signal_setup.store(true, Ordering::Relaxed);
Ok(())
}
fn execute_with_fork(&self, mut command: Command, config: &ShimConfig) -> Result<i32> {
self.setup_signal_handling(config)?;
match unsafe { fork() }.context("Failed to fork process")? {
ForkResult::Parent { child } => {
self.wait_for_child(child, config)
}
ForkResult::Child => {
let error = CommandExt::exec(&mut command);
eprintln!("Failed to exec: {}", error);
std::process::exit(1);
}
}
}
fn wait_for_child(&self, child_pid: Pid, config: &ShimConfig) -> Result<i32> {
let signal_config = config.signal_handling.as_ref();
let kill_on_exit = signal_config.and_then(|s| s.kill_on_exit).unwrap_or(true);
if kill_on_exit {
let child_pid_copy = child_pid;
ctrlc::set_handler(move || {
let _ = signal::killpg(Pid::from_raw(child_pid_copy.as_raw()), Signal::SIGTERM);
std::process::exit(130); })
.context("Failed to set Ctrl+C handler")?;
}
loop {
match waitpid(child_pid, None) {
Ok(WaitStatus::Exited(_, exit_code)) => {
return Ok(exit_code);
}
Ok(WaitStatus::Signaled(_, signal, _)) => {
return Ok(128 + signal as i32);
}
Ok(WaitStatus::Stopped(_, _)) => {
continue;
}
Ok(WaitStatus::Continued(_)) => {
continue;
}
Ok(_) => {
continue;
}
Err(nix::errno::Errno::EINTR) => {
continue;
}
Err(e) => {
return Err(anyhow::anyhow!("Failed to wait for child: {}", e));
}
}
}
}
fn execute_simple(&self, mut command: Command, config: &ShimConfig) -> Result<i32> {
self.setup_signal_handling(config)?;
let mut child = command.spawn().context("Failed to spawn child process")?;
let exit_status = child.wait().context("Failed to wait for child process")?;
Ok(exit_status.code().unwrap_or(1))
}
}
impl PlatformExecutorTrait for UnixExecutor {
fn execute(&self, command: Command, config: &ShimConfig) -> Result<i32> {
let signal_config = config.signal_handling.as_ref();
let use_fork = signal_config
.and_then(|s| s.forward_signals)
.unwrap_or(true);
if use_fork {
self.execute_with_fork(command, config)
} else {
self.execute_simple(command, config)
}
}
}
trait CommandExt {
fn exec(&mut self) -> std::io::Error;
}
impl CommandExt for Command {
fn exec(&mut self) -> std::io::Error {
use std::os::unix::process::CommandExt as StdCommandExt;
StdCommandExt::exec(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unix_executor_creation() {
let executor = UnixExecutor::new();
assert!(!executor.signal_setup.load(Ordering::Relaxed));
}
#[test]
fn test_signal_setup() {
let executor = UnixExecutor::new();
let config = ShimConfig {
path: "/bin/echo".to_string(),
args: None,
working_dir: None,
env: None,
hide_console: None,
run_as_admin: None,
signal_handling: None,
};
let result = executor.setup_signal_handling(&config);
assert!(result.is_ok());
assert!(executor.signal_setup.load(Ordering::Relaxed));
}
}