use super::run_trap;
use crate::Runtime;
use std::ops::ControlFlow::Continue;
use std::rc::Rc;
use yash_env::Env;
use yash_env::semantics::Result;
use yash_env::signal;
use yash_env::stack::Frame;
use yash_env::trap::Action;
use yash_env::trap::Condition;
use yash_env::trap::Origin;
#[cfg(doc)]
use yash_env::trap::TrapSet;
#[must_use]
pub async fn run_trap_if_caught<S: Runtime + 'static>(
env: &mut Env<S>,
signal: signal::Number,
) -> Option<Result> {
let trap_state = env.traps.take_signal_if_caught(signal)?;
let Action::Command(command) = &trap_state.action else {
return None;
};
let code = Rc::clone(command);
let origin = match &trap_state.origin {
Origin::Inherited | Origin::Subshell => panic!("user-defined trap must have origin"),
Origin::User(location) => location.clone(),
};
Some(run_trap(env, signal.into(), code, origin).await)
}
fn in_trap<S>(env: &Env<S>) -> bool {
env.stack
.iter()
.rev()
.take_while(|frame| **frame != Frame::Subshell)
.any(|frame| matches!(*frame, Frame::Trap(Condition::Signal(_))))
}
pub async fn run_traps_for_caught_signals<S: Runtime + 'static>(env: &mut Env<S>) -> Result {
env.poll_signals();
if in_trap(env) {
return Continue(());
}
while let Some((signal, state)) = env.traps.take_caught_signal() {
let Action::Command(command) = &state.action else {
continue;
};
let code = Rc::clone(command);
let origin = match &state.origin {
Origin::Inherited | Origin::Subshell => panic!("user-defined trap must have origin"),
Origin::User(location) => location.clone(),
};
run_trap(env, signal.into(), code, origin).await?;
}
Continue(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::echo_builtin;
use crate::tests::exit_builtin;
use assert_matches::assert_matches;
use futures_util::FutureExt;
use std::ops::ControlFlow::Break;
use std::pin::Pin;
use yash_env::builtin::Builtin;
use yash_env::semantics::Divert;
use yash_env::semantics::ExitStatus;
use yash_env::semantics::Field;
use yash_env::signal;
use yash_env::system::r#virtual::VirtualSystem;
use yash_env::system::r#virtual::{SIGINT, SIGTERM, SIGUSR1, SIGUSR2};
use yash_env::test_helper::assert_stdout;
use yash_env::trap::Action;
use yash_syntax::source::Location;
fn signal_env() -> (Env<VirtualSystem>, VirtualSystem) {
let system = VirtualSystem::default();
let mut env = Env::with_system(system.clone());
env.builtins.insert("echo", echo_builtin());
env.traps
.set_action(
&env.system,
SIGINT,
Action::Command("echo trapped".into()),
Location::dummy(""),
false,
)
.now_or_never()
.unwrap()
.unwrap();
(env, system)
}
fn raise_signal(system: &VirtualSystem, signal: signal::Number) {
let _ = system
.state
.borrow_mut()
.processes
.get_mut(&system.process_id)
.unwrap()
.raise_signal(signal);
}
#[test]
fn nothing_to_do_without_signals_caught() {
let (mut env, system) = signal_env();
let result = run_traps_for_caught_signals(&mut env)
.now_or_never()
.unwrap();
assert_eq!(result, Continue(()));
assert_stdout(&system.state, |stdout| assert_eq!(stdout, ""));
}
#[test]
fn running_trap() {
let (mut env, system) = signal_env();
raise_signal(&system, SIGINT);
let result = run_traps_for_caught_signals(&mut env)
.now_or_never()
.unwrap();
assert_eq!(result, Continue(()));
assert_stdout(&system.state, |stdout| assert_eq!(stdout, "trapped\n"));
}
#[test]
fn no_reentrance() {
let (mut env, system) = signal_env();
raise_signal(&system, SIGINT);
let mut env = env.push_frame(Frame::Trap(Condition::Signal(SIGTERM)));
let result = run_traps_for_caught_signals(&mut env)
.now_or_never()
.unwrap();
assert_eq!(result, Continue(()));
assert_stdout(&system.state, |stdout| assert_eq!(stdout, ""));
}
#[test]
fn allow_reentrance_in_exit_trap() {
let (mut env, system) = signal_env();
raise_signal(&system, SIGINT);
let mut env = env.push_frame(Frame::Trap(Condition::Exit));
let result = run_traps_for_caught_signals(&mut env)
.now_or_never()
.unwrap();
assert_eq!(result, Continue(()));
assert_stdout(&system.state, |stdout| assert_eq!(stdout, "trapped\n"));
}
#[test]
fn allow_reentrance_in_subshell() {
let (mut env, system) = signal_env();
raise_signal(&system, SIGINT);
let mut env = env.push_frame(Frame::Trap(Condition::Signal(SIGTERM)));
let mut env = env.push_frame(Frame::Subshell);
let result = run_traps_for_caught_signals(&mut env)
.now_or_never()
.unwrap();
assert_eq!(result, Continue(()));
assert_stdout(&system.state, |stdout| assert_eq!(stdout, "trapped\n"));
}
#[test]
fn stack_frame_in_trap_action() {
fn execute(
env: &mut Env<VirtualSystem>,
_args: Vec<Field>,
) -> Pin<Box<dyn Future<Output = yash_env::builtin::Result> + '_>> {
Box::pin(async move {
assert_matches!(&env.stack[0], Frame::Trap(Condition::Signal(SIGINT)));
Default::default()
})
}
let system = VirtualSystem::default();
let mut env = Env::with_system(system.clone());
env.builtins.insert(
"check",
Builtin::new(yash_env::builtin::Type::Mandatory, execute),
);
env.traps
.set_action(
&env.system,
SIGINT,
Action::Command("check".into()),
Location::dummy(""),
false,
)
.now_or_never()
.unwrap()
.unwrap();
raise_signal(&system, SIGINT);
let _ = run_traps_for_caught_signals(&mut env)
.now_or_never()
.unwrap();
}
#[test]
fn exit_status_is_restored_after_running_trap() {
let (mut env, system) = signal_env();
env.exit_status = ExitStatus(42);
raise_signal(&system, SIGINT);
let _ = run_traps_for_caught_signals(&mut env)
.now_or_never()
.unwrap();
assert_eq!(env.exit_status, ExitStatus(42));
}
#[test]
fn exit_status_inside_trap() {
let (mut env, system) = signal_env();
for signal in [SIGUSR1, SIGUSR2] {
env.traps
.set_action(
&env.system,
signal,
Action::Command("echo $?; echo $?".into()),
Location::dummy(""),
false,
)
.now_or_never()
.unwrap()
.unwrap();
}
env.exit_status = ExitStatus(123);
raise_signal(&system, SIGUSR1);
raise_signal(&system, SIGUSR2);
let _ = run_traps_for_caught_signals(&mut env)
.now_or_never()
.unwrap();
assert_stdout(&system.state, |stdout| {
assert_eq!(stdout, "123\n0\n123\n0\n")
});
}
#[test]
fn exit_from_trap_with_specified_exit_status() {
let (mut env, system) = signal_env();
env.builtins.insert("exit", exit_builtin());
env.traps
.set_action(
&env.system,
SIGUSR1,
Action::Command("echo; exit 56".into()),
Location::dummy(""),
false,
)
.now_or_never()
.unwrap()
.unwrap();
raise_signal(&system, SIGUSR1);
env.exit_status = ExitStatus(42);
let result = run_traps_for_caught_signals(&mut env)
.now_or_never()
.unwrap();
assert_eq!(result, Break(Divert::Exit(Some(ExitStatus(56)))));
assert_eq!(env.exit_status, ExitStatus(42));
}
#[test]
fn exit_from_trap_without_specified_exit_status() {
let (mut env, system) = signal_env();
env.builtins.insert("exit", exit_builtin());
env.traps
.set_action(
&env.system,
SIGUSR1,
Action::Command("echo; exit".into()),
Location::dummy(""),
false,
)
.now_or_never()
.unwrap()
.unwrap();
raise_signal(&system, SIGUSR1);
env.exit_status = ExitStatus(42);
let result = run_traps_for_caught_signals(&mut env)
.now_or_never()
.unwrap();
assert_eq!(result, Break(Divert::Exit(None)));
assert_eq!(env.exit_status, ExitStatus(42));
}
}