use crate::{
flog::flog,
io::{IoChain, IoStreams},
job_group::MaybeJobId,
parser::{Block, Parser},
prelude::*,
proc::Pid,
reader::reader_update_termsize,
signal::{signal_check_cancel, signal_handle, Signal},
};
use fish_common::{escape, ScopeGuard};
use fish_widestring::str2wcstring;
use std::sync::{
atomic::{AtomicBool, AtomicU32, Ordering},
Arc, Mutex,
};
pub enum EventType {
Any,
Signal,
Variable,
ProcessExit,
JobExit,
CallerExit,
Generic,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum EventDescription {
Any,
Signal { signal: Signal },
Variable { name: WString },
ProcessExit {
pid: Option<Pid>,
},
JobExit {
pid: Option<Pid>,
internal_job_id: u64,
},
CallerExit {
caller_id: u64,
},
Generic {
param: WString,
},
}
impl EventDescription {
fn str_param1(&self) -> Option<&wstr> {
match self {
EventDescription::Any
| EventDescription::Signal { .. }
| EventDescription::ProcessExit { .. }
| EventDescription::JobExit { .. }
| EventDescription::CallerExit { .. } => None,
EventDescription::Variable { name } => Some(name),
EventDescription::Generic { param } => Some(param),
}
}
fn name(&self) -> &'static wstr {
match self {
EventDescription::Any => L!("any"),
EventDescription::Signal { .. } => L!("signal"),
EventDescription::Variable { .. } => L!("variable"),
EventDescription::ProcessExit { .. } => L!("process-exit"),
EventDescription::JobExit { .. } => L!("job-exit"),
EventDescription::CallerExit { .. } => L!("caller-exit"),
EventDescription::Generic { .. } => L!("generic"),
}
}
fn matches_filter(&self, filter: &wstr) -> bool {
if filter.is_empty() {
return true;
}
match self {
EventDescription::Any => false,
EventDescription::ProcessExit { .. }
| EventDescription::JobExit { .. }
| EventDescription::CallerExit { .. }
if filter == "exit" =>
{
true
}
_ => filter == self.name(),
}
}
}
impl From<&EventDescription> for EventType {
fn from(desc: &EventDescription) -> Self {
match desc {
EventDescription::Any => EventType::Any,
EventDescription::Signal { .. } => EventType::Signal,
EventDescription::Variable { .. } => EventType::Variable,
EventDescription::ProcessExit { .. } => EventType::ProcessExit,
EventDescription::JobExit { .. } => EventType::JobExit,
EventDescription::CallerExit { .. } => EventType::CallerExit,
EventDescription::Generic { .. } => EventType::Generic,
}
}
}
#[derive(Debug)]
pub struct EventHandler {
pub desc: EventDescription,
pub function_name: WString,
pub removed: AtomicBool,
pub fired: AtomicBool,
}
impl EventHandler {
pub fn new(desc: EventDescription, name: Option<WString>) -> Self {
Self {
desc,
function_name: name.unwrap_or_default(),
removed: AtomicBool::new(false),
fired: AtomicBool::new(false),
}
}
fn is_one_shot(&self) -> bool {
match self.desc {
EventDescription::ProcessExit { pid } => pid.is_some(),
EventDescription::JobExit { pid, .. } => pid.is_some(),
EventDescription::CallerExit { .. } => true,
EventDescription::Signal { .. }
| EventDescription::Variable { .. }
| EventDescription::Generic { .. }
| EventDescription::Any => false,
}
}
fn matches(&self, event: &Event) -> bool {
match (&self.desc, &event.desc) {
(EventDescription::Any, _) => true,
(
EventDescription::Signal { signal },
EventDescription::Signal { signal: ev_signal },
) => signal == ev_signal,
(EventDescription::Variable { name }, EventDescription::Variable { name: ev_name }) => {
name == ev_name
}
(
EventDescription::ProcessExit { pid },
EventDescription::ProcessExit { pid: ev_pid },
) => pid.is_none() || pid == ev_pid,
(
EventDescription::JobExit {
pid,
internal_job_id,
},
EventDescription::JobExit {
internal_job_id: ev_internal_job_id,
..
},
) => pid.is_none() || internal_job_id == ev_internal_job_id,
(
EventDescription::CallerExit { caller_id },
EventDescription::CallerExit {
caller_id: ev_caller_id,
},
) => caller_id == ev_caller_id,
(
EventDescription::Generic { param },
EventDescription::Generic { param: ev_param },
) => param == ev_param,
(_, _) => false,
}
}
}
type EventHandlerList = Vec<Arc<EventHandler>>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Event {
desc: EventDescription,
arguments: Vec<WString>,
}
impl Event {
pub fn generic(desc: WString) -> Self {
Self {
desc: EventDescription::Generic { param: desc },
arguments: vec![],
}
}
pub fn variable_erase(name: WString) -> Self {
Self {
desc: EventDescription::Variable { name: name.clone() },
arguments: vec!["VARIABLE".into(), "ERASE".into(), name],
}
}
pub fn variable_set(name: WString) -> Self {
Self {
desc: EventDescription::Variable { name: name.clone() },
arguments: vec!["VARIABLE".into(), "SET".into(), name],
}
}
pub fn process_exit(pid: Pid, status: i32) -> Self {
Self {
desc: EventDescription::ProcessExit { pid: Some(pid) },
arguments: vec![
"PROCESS_EXIT".into(),
pid.to_string().into(),
status.to_string().into(),
],
}
}
pub fn job_exit(pgid: Pid, jid: u64) -> Self {
Self {
desc: EventDescription::JobExit {
pid: Some(pgid),
internal_job_id: jid,
},
arguments: vec![
"JOB_EXIT".into(),
pgid.to_string().into(),
"0".into(), ],
}
}
pub fn caller_exit(internal_job_id: u64, job_id: MaybeJobId) -> Self {
Self {
desc: EventDescription::CallerExit {
caller_id: internal_job_id,
},
arguments: vec![
"JOB_EXIT".into(),
job_id.to_wstring(),
"0".into(), ],
}
}
fn is_blocked(&self, parser: &Parser) -> bool {
for block in parser.blocks_iter_rev() {
if block.event_blocks {
return true;
}
}
parser.global_event_blocks.load(Ordering::Relaxed) != 0
}
}
const SIGNAL_COUNT: usize = 64;
struct PendingSignals {
counter: AtomicU32,
received: [AtomicBool; SIGNAL_COUNT],
last_counter: Mutex<u32>,
}
impl PendingSignals {
pub fn mark(&self, sig: libc::c_int) {
if let Some(received) = self.received.get(usize::try_from(sig).unwrap()) {
received.store(true, Ordering::Relaxed);
self.counter.fetch_add(1, Ordering::Relaxed);
}
}
pub fn acquire_pending(&self) -> u64 {
let mut current = self
.last_counter
.lock()
.expect("mutex should not be poisoned");
let count = self.counter.load(Ordering::Acquire);
if count == *current {
return 0;
}
*current = count;
let mut result = 0;
for (i, received) in self.received.iter().enumerate() {
if received.load(Ordering::Relaxed) {
result |= 1_u64 << i;
received.store(false, Ordering::Relaxed);
}
}
result
}
}
#[allow(clippy::declare_interior_mutable_const)]
const ATOMIC_BOOL_FALSE: AtomicBool = AtomicBool::new(false);
#[allow(clippy::declare_interior_mutable_const)]
const ATOMIC_U32_0: AtomicU32 = AtomicU32::new(0);
static PENDING_SIGNALS: PendingSignals = PendingSignals {
counter: AtomicU32::new(0),
received: [ATOMIC_BOOL_FALSE; SIGNAL_COUNT],
last_counter: Mutex::new(0),
};
static EVENT_HANDLERS: Mutex<EventHandlerList> = Mutex::new(Vec::new());
static OBSERVED_SIGNALS: [AtomicU32; SIGNAL_COUNT] = [ATOMIC_U32_0; SIGNAL_COUNT];
static BLOCKED_EVENTS: Mutex<Vec<Event>> = Mutex::new(Vec::new());
fn inc_signal_observed(sig: Signal) {
if let Some(sig) = OBSERVED_SIGNALS.get(usize::from(sig)) {
sig.fetch_add(1, Ordering::Relaxed);
}
}
fn dec_signal_observed(sig: Signal) {
if let Some(sig) = OBSERVED_SIGNALS.get(usize::from(sig)) {
sig.fetch_sub(1, Ordering::Relaxed);
}
}
pub fn is_signal_observed(sig: libc::c_int) -> bool {
OBSERVED_SIGNALS
.get(usize::try_from(sig).unwrap())
.is_some_and(|s| s.load(Ordering::Relaxed) > 0)
}
pub fn get_desc(parser: &Parser, evt: &Event) -> WString {
let s = match &evt.desc {
EventDescription::Signal { signal } => {
format!("signal handler for {} ({})", signal.name(), signal.desc(),)
}
EventDescription::Variable { name } => format!("handler for variable '{name}'"),
EventDescription::ProcessExit { pid: None } => "exit handler for any process".to_owned(),
EventDescription::ProcessExit { pid: Some(pid) } => {
format!("exit handler for process {pid}")
}
EventDescription::JobExit { pid, .. } => {
if let Some(pid) = pid {
if let Some(job) = parser.job_get_from_pid(*pid) {
format!("exit handler for job {}, '{}'", job.job_id(), job.command())
} else {
format!("exit handler for job with pid {pid}")
}
} else {
"exit handler for any job".to_owned()
}
}
EventDescription::CallerExit { .. } => {
"exit handler for command substitution caller".to_owned()
}
EventDescription::Generic { param } => format!("handler for generic event '{param}'"),
EventDescription::Any => unreachable!(),
};
str2wcstring(&s)
}
pub fn add_handler(eh: EventHandler) {
if let EventDescription::Signal { signal } = eh.desc {
signal_handle(signal);
inc_signal_observed(signal);
}
EVENT_HANDLERS
.lock()
.expect("event handler list should not be poisoned")
.push(Arc::new(eh));
}
fn remove_handlers_if(pred: impl Fn(&EventHandler) -> bool) -> usize {
let mut handlers = EVENT_HANDLERS
.lock()
.expect("event handler list should not be poisoned");
let mut removed = 0;
for i in (0..handlers.len()).rev() {
let handler = &handlers[i];
if pred(handler) {
handler.removed.store(true, Ordering::Relaxed);
if let EventDescription::Signal { signal } = handler.desc {
dec_signal_observed(signal);
}
handlers.remove(i);
removed += 1;
}
}
removed
}
pub fn remove_function_handlers(name: &wstr) -> usize {
remove_handlers_if(|h| h.function_name == name)
}
pub fn get_function_handlers(name: &wstr) -> EventHandlerList {
EVENT_HANDLERS
.lock()
.expect("event handler list should not be poisoned")
.iter()
.filter(|h| h.function_name == name)
.cloned()
.collect()
}
fn fire_internal(parser: &Parser, event: &Event) {
let _saved = parser.push_scope(|s| {
s.is_event = true;
s.suppress_fish_trace = true;
});
let fire: Vec<_> = EVENT_HANDLERS
.lock()
.expect("event handler list should not be poisoned")
.iter()
.filter(|h| h.matches(event))
.cloned()
.collect();
let mut fired_one_shot = false;
for handler in fire {
if handler.removed.load(Ordering::Relaxed) {
continue;
}
let mut buffer = handler.function_name.clone();
for arg in &event.arguments {
buffer.push(' ');
buffer.push_utfstr(&escape(arg));
}
let _non_interactive = parser.push_scope(|s| s.is_interactive = false);
let saved_statuses = parser.get_last_statuses();
let _cleanup = ScopeGuard::new((), |()| {
parser.set_last_statuses(saved_statuses);
});
flog!(
event,
"Firing event '",
event.desc.str_param1().unwrap_or(L!("")),
"' to handler '",
handler.function_name,
"'"
);
let b = parser.push_block(Block::event_block(event.clone()));
parser.eval(&buffer, &IoChain::new());
parser.pop_block(b);
handler.fired.store(true, Ordering::Relaxed);
fired_one_shot |= handler.is_one_shot();
}
if fired_one_shot {
remove_handlers_if(|h| h.fired.load(Ordering::Relaxed) && h.is_one_shot());
}
}
pub fn fire_delayed(parser: &Parser) {
if parser.scope().is_event {
return;
}
if signal_check_cancel() != 0 {
return;
}
let mut to_send = std::mem::take(&mut *BLOCKED_EVENTS.lock().expect("Mutex poisoned!"));
let mut signals: u64 = PENDING_SIGNALS.acquire_pending();
while signals != 0 {
let sig = signals.trailing_zeros() as i32;
signals &= !(1_u64 << sig);
let sig = Signal::new(sig);
if sig == libc::SIGWINCH {
reader_update_termsize(parser);
}
let event = Event {
desc: EventDescription::Signal { signal: sig },
arguments: vec![sig.name().into()],
};
to_send.push(event);
}
let mut blocked_events = None;
for event in to_send {
if event.is_blocked(parser) {
if blocked_events.is_none() {
blocked_events = Some(BLOCKED_EVENTS.lock().expect("Mutex poisoned"));
}
blocked_events.as_mut().unwrap().push(event);
} else {
fire_internal(parser, &event);
}
}
}
pub fn enqueue_signal(signal: libc::c_int) {
PENDING_SIGNALS.mark(signal);
}
pub fn fire(parser: &Parser, event: Event) {
fire_delayed(parser);
if event.is_blocked(parser) {
BLOCKED_EVENTS.lock().expect("Mutex poisoned!").push(event);
} else {
fire_internal(parser, &event);
}
}
pub const EVENT_FILTER_NAMES: [&wstr; 7] = [
L!("signal"),
L!("variable"),
L!("exit"),
L!("process-exit"),
L!("job-exit"),
L!("caller-exit"),
L!("generic"),
];
pub fn print(streams: &mut IoStreams, type_filter: &wstr) {
let mut tmp = EVENT_HANDLERS
.lock()
.expect("event handler list should not be poisoned")
.clone();
tmp.sort_by(|e1, e2| e1.desc.cmp(&e2.desc));
let mut last_type = std::mem::discriminant(&EventDescription::Any);
for evt in tmp {
if !evt.desc.matches_filter(type_filter) {
continue;
}
if last_type != std::mem::discriminant(&evt.desc) {
if last_type != std::mem::discriminant(&EventDescription::Any) {
streams.out.append(L!("\n"));
}
last_type = std::mem::discriminant(&evt.desc);
streams.out.append(&sprintf!("Event %s\n", evt.desc.name()));
}
match &evt.desc {
EventDescription::Signal { signal } => {
let name: WString = signal.name().into();
streams
.out
.append(&sprintf!("%s %s\n", name, evt.function_name));
}
EventDescription::ProcessExit { .. } | EventDescription::JobExit { .. } => {}
EventDescription::CallerExit { .. } => {
streams
.out
.append(&sprintf!("caller-exit %s\n", evt.function_name));
}
EventDescription::Variable { name: param } | EventDescription::Generic { param } => {
streams
.out
.append(&sprintf!("%s %s\n", param, evt.function_name));
}
EventDescription::Any => unreachable!(),
}
}
}
pub fn fire_generic(parser: &Parser, name: WString, arguments: Vec<WString>) {
fire(
parser,
Event {
desc: EventDescription::Generic { param: name },
arguments,
},
);
}