use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Mutex, OnceLock};
use super::*;
const MAX_TRAP_SIG: usize = 128;
const MAX_SIGNAL_REGISTRATIONS: usize = 64;
static SIGNAL_REGISTRATION_TOKENS: [AtomicU64; MAX_SIGNAL_REGISTRATIONS] =
[const { AtomicU64::new(0) }; MAX_SIGNAL_REGISTRATIONS];
static SIGNAL_INTEREST_LO: [AtomicU64; MAX_SIGNAL_REGISTRATIONS] =
[const { AtomicU64::new(0) }; MAX_SIGNAL_REGISTRATIONS];
static SIGNAL_INTEREST_HI: [AtomicU64; MAX_SIGNAL_REGISTRATIONS] =
[const { AtomicU64::new(0) }; MAX_SIGNAL_REGISTRATIONS];
static PENDING_SIGNAL_COUNTS: [AtomicUsize; MAX_SIGNAL_REGISTRATIONS * MAX_TRAP_SIG] =
[const { AtomicUsize::new(0) }; MAX_SIGNAL_REGISTRATIONS * MAX_TRAP_SIG];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum TrapDisposition {
Catch,
Ignore,
}
struct SignalInstallState {
saved_handler: libc::sighandler_t,
catch_tokens: HashSet<u64>,
ignore_tokens: HashSet<u64>,
}
#[derive(Default)]
struct SignalHub {
installed: HashMap<i32, SignalInstallState>,
}
fn signal_hub() -> &'static Mutex<SignalHub> {
static HUB: OnceLock<Mutex<SignalHub>> = OnceLock::new();
HUB.get_or_init(|| Mutex::new(SignalHub::default()))
}
fn signal_bit(sig: i32) -> Option<(&'static [AtomicU64; MAX_SIGNAL_REGISTRATIONS], u64)> {
if sig <= 0 || (sig as usize) >= MAX_TRAP_SIG {
return None;
}
let sig = sig as usize;
if sig < 64 {
Some((&SIGNAL_INTEREST_LO, 1_u64 << sig))
} else {
Some((&SIGNAL_INTEREST_HI, 1_u64 << (sig - 64)))
}
}
fn pending_signal_count(slot: usize, sig: usize) -> &'static AtomicUsize {
&PENDING_SIGNAL_COUNTS[slot * MAX_TRAP_SIG + sig]
}
fn clear_pending_signals_in_slot(slot: usize) {
for sig in 1..MAX_TRAP_SIG {
pending_signal_count(slot, sig).store(0, Ordering::Relaxed);
}
}
fn find_signal_registration(token: u64) -> Option<usize> {
SIGNAL_REGISTRATION_TOKENS
.iter()
.position(|slot| slot.load(Ordering::Relaxed) == token)
}
fn signal_registration_for(token: u64) -> Option<usize> {
if token == 0 {
return None;
}
if let Some(slot) = find_signal_registration(token) {
return Some(slot);
}
for (idx, slot) in SIGNAL_REGISTRATION_TOKENS.iter().enumerate() {
if slot
.compare_exchange(0, token, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
return Some(idx);
}
}
None
}
fn set_signal_interest(token: u64, sig: i32, interested: bool) {
let Some((interests, bit)) = signal_bit(sig) else {
return;
};
if interested {
let Some(slot) = signal_registration_for(token) else {
return;
};
interests[slot].fetch_or(bit, Ordering::Relaxed);
} else {
let Some(slot) = find_signal_registration(token) else {
return;
};
interests[slot].fetch_and(!bit, Ordering::Relaxed);
pending_signal_count(slot, sig as usize).store(0, Ordering::Relaxed);
}
}
fn clear_pending_signal_for(token: u64, sig: i32) {
if sig <= 0 || (sig as usize) >= MAX_TRAP_SIG {
return;
}
if let Some(slot) = find_signal_registration(token) {
pending_signal_count(slot, sig as usize).store(0, Ordering::Relaxed);
}
}
fn desired_signal_handler(state: &SignalInstallState) -> libc::sighandler_t {
if state.catch_tokens.is_empty() {
libc::SIG_IGN
} else {
trap_signal_handler as *const () as libc::sighandler_t
}
}
fn register_process_signal(token: u64, sig: i32, disposition: TrapDisposition) {
let mut hub = signal_hub()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let desired = match disposition {
TrapDisposition::Catch => trap_signal_handler as *const () as libc::sighandler_t,
TrapDisposition::Ignore => libc::SIG_IGN,
};
let state = hub
.installed
.entry(sig)
.or_insert_with(|| SignalInstallState {
saved_handler: unsafe { libc::signal(sig, desired) },
catch_tokens: HashSet::new(),
ignore_tokens: HashSet::new(),
});
match disposition {
TrapDisposition::Catch => {
state.catch_tokens.insert(token);
state.ignore_tokens.remove(&token);
}
TrapDisposition::Ignore => {
state.ignore_tokens.insert(token);
state.catch_tokens.remove(&token);
}
}
let desired = desired_signal_handler(state);
unsafe { libc::signal(sig, desired) };
}
fn unregister_process_signal(token: u64, sig: i32) {
let mut hub = signal_hub()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let Some(state) = hub.installed.get_mut(&sig) else {
return;
};
state.catch_tokens.remove(&token);
state.ignore_tokens.remove(&token);
if state.catch_tokens.is_empty() && state.ignore_tokens.is_empty() {
let saved_handler = state.saved_handler;
hub.installed.remove(&sig);
unsafe { libc::signal(sig, saved_handler) };
} else {
let desired = desired_signal_handler(state);
unsafe { libc::signal(sig, desired) };
}
}
fn register_trap_signal(
token: u64,
sig: i32,
disposition: TrapDisposition,
install_process_handler: bool,
) {
clear_pending_signal_for(token, sig);
set_signal_interest(token, sig, matches!(disposition, TrapDisposition::Catch));
if install_process_handler && sig > 0 {
register_process_signal(token, sig, disposition);
}
}
fn unregister_trap_signal(token: u64, sig: i32) {
clear_pending_signal_for(token, sig);
set_signal_interest(token, sig, false);
if sig > 0 {
unregister_process_signal(token, sig);
}
}
#[cfg(any(feature = "embed", test))]
fn unregister_all_trap_signals(token: u64) {
if let Some(slot) = find_signal_registration(token) {
SIGNAL_INTEREST_LO[slot].store(0, Ordering::Relaxed);
SIGNAL_INTEREST_HI[slot].store(0, Ordering::Relaxed);
clear_pending_signals_in_slot(slot);
SIGNAL_REGISTRATION_TOKENS[slot].store(0, Ordering::Relaxed);
}
let mut hub = signal_hub()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let signals: Vec<i32> = hub.installed.keys().copied().collect();
for sig in signals {
let Some(state) = hub.installed.get_mut(&sig) else {
continue;
};
state.catch_tokens.remove(&token);
state.ignore_tokens.remove(&token);
if state.catch_tokens.is_empty() && state.ignore_tokens.is_empty() {
let saved_handler = state.saved_handler;
hub.installed.remove(&sig);
unsafe { libc::signal(sig, saved_handler) };
} else {
let desired = desired_signal_handler(state);
unsafe { libc::signal(sig, desired) };
}
}
}
pub(super) extern "C" fn trap_signal_handler(sig: i32) {
let Some((interests, bit)) = signal_bit(sig) else {
return;
};
for slot in 0..MAX_SIGNAL_REGISTRATIONS {
if SIGNAL_REGISTRATION_TOKENS[slot].load(Ordering::Relaxed) == 0 {
continue;
}
if (interests[slot].load(Ordering::Relaxed) & bit) != 0 {
pending_signal_count(slot, sig as usize).fetch_add(1, Ordering::Relaxed);
}
}
}
#[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
pub(crate) fn clear_all_pending_traps() {
for pending in PENDING_SIGNAL_COUNTS.iter() {
pending.store(0, Ordering::Relaxed);
}
}
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
pub(crate) fn clear_pending_traps(state: &ShellState) {
if let Some(slot) = find_signal_registration(state.trap_table.owner_id) {
clear_pending_signals_in_slot(slot);
}
}
pub(super) fn run_pending_traps<R: Runtime>(state: &mut ShellState, runtime: &mut R) {
let Some(slot) = find_signal_registration(state.trap_table.owner_id) else {
return;
};
let last_status = state.last_status;
for sig in 1..MAX_TRAP_SIG {
let Some(action) = state.trap_table.traps.get(&(sig as i32)).cloned() else {
continue;
};
let pending = pending_signal_count(slot, sig).swap(0, Ordering::Relaxed);
if pending == 0 {
continue;
}
if action.is_empty() {
continue;
}
for _ in 0..pending {
let _ = run_string(state, runtime, &action);
}
state.set_last_status(last_status);
}
}
#[cfg(any(
feature = "frontend",
all(test, feature = "test-support", feature = "unix-runtime")
))]
pub(super) fn install_restored_traps(state: &mut ShellState) {
for (sig, action) in state.trap_table.traps.clone() {
if sig <= 0 {
continue;
}
let disposition = if action.is_empty() {
TrapDisposition::Ignore
} else {
TrapDisposition::Catch
};
register_trap_signal(
state.trap_table.owner_id,
sig,
disposition,
state.manage_signals && !state.process_isolated,
);
}
}
pub(super) fn builtin_trap(state: &mut ShellState, args: &[String]) -> i32 {
let args = if args.first().is_some_and(|arg| arg == "--") {
&args[1..]
} else {
args
};
if args.is_empty() {
let mut entries: Vec<(i32, String)> = state
.trap_table
.traps
.iter()
.map(|(sig, action)| (*sig, action.clone()))
.collect();
entries.sort_by_key(|(sig, _)| *sig);
for (sig, action) in entries {
if shell_outln(
state,
&format!(
"trap -- '{}' {}",
action.replace('\'', "'\\''"),
trap_signal_name(sig)
),
)
.is_err()
{
return 1;
}
}
return 0;
}
if args.len() == 1 {
let sig = match parse_trap_signal(&args[0]) {
Some(sig) => sig,
None => {
shell_errln(
state,
&format!("trap: {}: invalid signal specification", args[0]),
);
return 1;
}
};
if is_numeric_trap_operand(&args[0]) {
state.trap_table.traps.remove(&sig);
if sig == TRAP_EXIT {
state.trap_table.inherited_exit_trap = false;
}
unregister_trap_signal(state.trap_table.owner_id, sig);
return 0;
}
if let Some(action) = state.trap_table.traps.get(&sig)
&& shell_outln(state, &render_trap(action, sig)).is_err()
{
return 1;
}
return 0;
}
let action = &args[0];
for sig_name in &args[1..] {
let sig = match parse_trap_signal(sig_name) {
Some(sig) => sig,
None => {
shell_errln(
state,
&format!("trap: {sig_name}: invalid signal specification"),
);
return 1;
}
};
if sig == libc::SIGKILL || sig == libc::SIGSTOP {
shell_errln(
state,
"trap: setting a trap for SIGKILL or SIGSTOP produces undefined results",
);
return 1;
}
if action == "-" {
state.trap_table.traps.remove(&sig);
if sig == TRAP_EXIT {
state.trap_table.inherited_exit_trap = false;
}
unregister_trap_signal(state.trap_table.owner_id, sig);
} else if action.is_empty() {
state.trap_table.traps.insert(sig, action.clone());
if sig == TRAP_EXIT {
state.trap_table.inherited_exit_trap = false;
}
register_trap_signal(
state.trap_table.owner_id,
sig,
TrapDisposition::Ignore,
state.manage_signals && !state.process_isolated,
);
} else {
state.trap_table.traps.insert(sig, action.clone());
if sig == TRAP_EXIT {
state.trap_table.inherited_exit_trap = false;
}
register_trap_signal(
state.trap_table.owner_id,
sig,
TrapDisposition::Catch,
state.manage_signals && !state.process_isolated,
);
}
}
0
}
fn is_numeric_trap_operand(arg: &str) -> bool {
arg.chars().all(|ch| ch.is_ascii_digit())
}
fn render_trap(action: &str, sig: i32) -> String {
format!(
"trap -- '{}' {}",
action.replace('\'', "'\\''"),
trap_signal_name(sig)
)
}
pub(super) fn parse_trap_signal(sig: &str) -> Option<i32> {
if sig == "0" || sig == "EXIT" {
return Some(TRAP_EXIT);
}
if let Ok(num) = sig.parse::<i32>() {
return (num >= 0 && (num as usize) < MAX_TRAP_SIG).then_some(num);
}
let upper = sig.strip_prefix("SIG").unwrap_or(sig).to_ascii_uppercase();
match upper.as_str() {
"HUP" => Some(libc::SIGHUP),
"INT" => Some(libc::SIGINT),
"QUIT" => Some(libc::SIGQUIT),
"ABRT" => Some(libc::SIGABRT),
"ALRM" => Some(libc::SIGALRM),
"TERM" => Some(libc::SIGTERM),
"CHLD" => Some(libc::SIGCHLD),
"CONT" => Some(libc::SIGCONT),
"FPE" => Some(libc::SIGFPE),
"ILL" => Some(libc::SIGILL),
"KILL" => Some(libc::SIGKILL),
"PIPE" => Some(libc::SIGPIPE),
"SEGV" => Some(libc::SIGSEGV),
"STOP" => Some(libc::SIGSTOP),
"TSTP" => Some(libc::SIGTSTP),
"TTIN" => Some(libc::SIGTTIN),
"TTOU" => Some(libc::SIGTTOU),
"USR1" => Some(libc::SIGUSR1),
"USR2" => Some(libc::SIGUSR2),
"TRAP" => Some(libc::SIGTRAP),
"URG" => Some(libc::SIGURG),
"XCPU" => Some(libc::SIGXCPU),
"XFSZ" => Some(libc::SIGXFSZ),
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
"BUS" => Some(libc::SIGBUS),
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
"PROF" => Some(libc::SIGPROF),
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
"SYS" => Some(libc::SIGSYS),
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
"VTALRM" => Some(libc::SIGVTALRM),
_ => None,
}
}
pub(super) fn trap_signal_name(sig: i32) -> &'static str {
match sig {
TRAP_EXIT => "EXIT",
libc::SIGABRT => "ABRT",
libc::SIGALRM => "ALRM",
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
libc::SIGBUS => "BUS",
libc::SIGCHLD => "CHLD",
libc::SIGCONT => "CONT",
libc::SIGFPE => "FPE",
libc::SIGHUP => "HUP",
libc::SIGILL => "ILL",
libc::SIGINT => "INT",
libc::SIGKILL => "KILL",
libc::SIGPIPE => "PIPE",
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
libc::SIGPROF => "PROF",
libc::SIGQUIT => "QUIT",
libc::SIGSEGV => "SEGV",
libc::SIGSTOP => "STOP",
libc::SIGTERM => "TERM",
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
libc::SIGSYS => "SYS",
libc::SIGTRAP => "TRAP",
libc::SIGTSTP => "TSTP",
libc::SIGTTIN => "TTIN",
libc::SIGTTOU => "TTOU",
libc::SIGURG => "URG",
libc::SIGUSR1 => "USR1",
libc::SIGUSR2 => "USR2",
#[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
libc::SIGVTALRM => "VTALRM",
libc::SIGXCPU => "XCPU",
libc::SIGXFSZ => "XFSZ",
_ => "?",
}
}
pub(crate) fn run_exit_trap<R: Runtime>(state: &mut ShellState, runtime: &mut R) {
if state.trap_table.inherited_exit_trap {
state.trap_table.traps.remove(&TRAP_EXIT);
state.trap_table.inherited_exit_trap = false;
return;
}
let Some(action) = state.trap_table.traps.remove(&TRAP_EXIT) else {
return;
};
let saved_exit_code = state.exit_code;
let saved_control_flow = state.control_flow;
state.exit_code = -1;
state.control_flow = ControlFlow::None;
let _ = run_string(state, runtime, &action);
if state.exit_code < 0 {
state.exit_code = saved_exit_code;
state.control_flow = saved_control_flow;
}
}
#[cfg(any(feature = "embed", test))]
pub(super) fn restore_trap_signals(state: &mut ShellState) {
unregister_all_trap_signals(state.trap_table.owner_id);
}