Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions brush-builtins/src/kill.rs
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,16 @@ fn print_signals(

let signal = if let Ok(n) = s.parse::<i32>() {
// bash compatibility. `SIGHUP` -> `HUP`
TrapSignal::try_from(n).map(|s| {
PrintSignal::Name(s.as_str().strip_prefix("SIG").unwrap_or(s.as_str()))
})
TrapSignal::try_from(n)
.or_else(|error| {
n.checked_sub(128)
.filter(|signal| *signal > 0)
.ok_or(error)
.and_then(TrapSignal::try_from)
})
.map(|s| {
PrintSignal::Name(s.as_str().strip_prefix("SIG").unwrap_or(s.as_str()))
})
} else {
TrapSignal::try_from(s.as_str()).map(|sig| {
i32::try_from(sig).map_or(PrintSignal::Name(sig.as_str()), PrintSignal::Num)
Expand Down
2 changes: 1 addition & 1 deletion brush-core/src/shell/traps.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ impl<SE: crate::extensions::ShellExtensions> crate::Shell<SE> {
// EXIT and system signals are always inherited — i.e. their visibility is
// not gated by errtrace/functrace options. (The actual trap *state* for
// subshells is managed separately via `Shell::clone`.)
TrapSignal::Exit | TrapSignal::Signal(_) => true,
TrapSignal::Exit | TrapSignal::Signal(_) | TrapSignal::RealtimeSignal(_) => true,
}
}
}
2 changes: 1 addition & 1 deletion brush-core/src/sys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub(crate) mod wasm;
#[cfg(target_family = "wasm")]
pub(crate) use wasm as platform;

#[cfg(not(unix))]
#[cfg(any(not(unix), test))]
pub(crate) mod stubs;

#[cfg(any(unix, windows))]
Expand Down
26 changes: 23 additions & 3 deletions brush-core/src/sys/stubs/signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,16 @@ impl Signal {
""
}

/// Creates a `Signal` from a string representation.
pub fn from_str(s: &str) -> Result<Self, error::Error> {
/// Returns the platform signal number.
pub const fn number(self) -> i32 {
match self {}
}
}

impl std::str::FromStr for Signal {
type Err = error::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Err(error::ErrorKind::InvalidSignal(s.into()).into())
}
}
Expand All @@ -32,7 +40,19 @@ impl TryFrom<i32> for Signal {
}
}

pub(crate) fn ignored_signals() -> &'static [(i32, String)] {
pub(crate) const fn realtime_signal_definitions() -> &'static [(i32, String)] {
&[]
}

pub(crate) const fn realtime_signal_from_name(_name: &str) -> Option<i32> {
None
}

pub(crate) const fn realtime_signal_name(_number: i32) -> Option<&'static str> {
None
}

pub(crate) const fn ignored_signals() -> &'static [(i32, String)] {
&[]
}

Expand Down
171 changes: 150 additions & 21 deletions brush-core/src/sys/unix/signal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ pub(crate) fn ignored_signals() -> &'static [(i32, String)] {
}

fn query_ignored_signals() -> Vec<(i32, String)> {
let signals = Signal::iterator().map(|signal| (signal as i32, signal.as_str().to_owned()));

#[cfg(target_os = "linux")]
let signals = signals.chain(realtime_signals());

signals
Signal::iterator()
.map(|signal| (signal as i32, signal.as_str().to_owned()))
.chain(
realtime_signal_definitions()
.iter()
.map(|(number, name)| (*number, name.clone())),
)
.filter_map(|(number, name)| match signal_is_ignored(number) {
Ok(true) => Some((number, name)),
Ok(false) | Err(_) => None,
Expand All @@ -55,23 +56,71 @@ fn signal_is_ignored(signal: i32) -> Result<bool, error::Error> {
}

#[cfg(target_os = "linux")]
fn realtime_signals() -> impl Iterator<Item = (i32, String)> {
pub(crate) fn realtime_signal_definitions() -> &'static [(i32, String)] {
static SIGNALS: std::sync::OnceLock<Vec<(i32, String)>> = std::sync::OnceLock::new();

SIGNALS.get_or_init(|| {
let min = nix::libc::SIGRTMIN();
let max = nix::libc::SIGRTMAX();
let midpoint = min + (max - min) / 2;

(min..=max)
.map(|signal| {
let name = if signal == min {
"SIGRTMIN".to_owned()
} else if signal <= midpoint {
format!("SIGRTMIN+{}", signal - min)
} else if signal == max {
"SIGRTMAX".to_owned()
} else {
format!("SIGRTMAX-{}", max - signal)
};
(signal, name)
})
.collect()
})
}

#[cfg(not(target_os = "linux"))]
pub(crate) const fn realtime_signal_definitions() -> &'static [(i32, String)] {
&[]
}

#[cfg(target_os = "linux")]
pub(crate) fn realtime_signal_from_name(name: &str) -> Option<i32> {
let min = nix::libc::SIGRTMIN();
let max = nix::libc::SIGRTMAX();
let midpoint = min + (max - min) / 2;

(min..=max).map(move |signal| {
let name = if signal == min {
"SIGRTMIN".to_owned()
} else if signal <= midpoint {
format!("SIGRTMIN+{}", signal - min)
} else if signal == max {
"SIGRTMAX".to_owned()
} else {
format!("SIGRTMAX-{}", max - signal)
};
(signal, name)
})
if name == "SIGRTMIN" {
min
} else if name == "SIGRTMAX" {
max
} else if let Some(offset) = name
.strip_prefix("SIGRTMIN+")
.and_then(|offset| offset.parse::<i32>().ok())
{
min.checked_add(offset)
.filter(|number| (min..=max).contains(number))?
} else {
let offset = name
.strip_prefix("SIGRTMAX-")
.and_then(|offset| offset.parse::<i32>().ok())?;
max.checked_sub(offset)
.filter(|number| (midpoint < *number) && (*number < max))?
}
.into()
}

#[cfg(not(target_os = "linux"))]
pub(crate) const fn realtime_signal_from_name(_name: &str) -> Option<i32> {
None
}

pub(crate) fn realtime_signal_name(number: i32) -> Option<&'static str> {
realtime_signal_definitions()
.iter()
.find_map(|(candidate, name)| (*candidate == number).then_some(name.as_str()))
}

pub(crate) fn continue_process(pid: sys::process::ProcessId) -> Result<(), error::Error> {
Expand All @@ -90,7 +139,8 @@ pub fn kill_process(
signal: traps::TrapSignal,
) -> Result<(), error::Error> {
let translated_signal = match signal {
traps::TrapSignal::Signal(signal) => signal,
traps::TrapSignal::Signal(signal) => signal as i32,
traps::TrapSignal::RealtimeSignal(signal) => signal.number(),
traps::TrapSignal::Debug
| traps::TrapSignal::Err
| traps::TrapSignal::Exit
Expand All @@ -99,7 +149,8 @@ pub fn kill_process(
}
};

nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), translated_signal)
// SAFETY: `kill` does not dereference pointers; both arguments are plain integers.
nix::errno::Errno::result(unsafe { nix::libc::kill(pid, translated_signal) })
.map_err(|_errno| error::ErrorKind::FailedToSendSignal)?;

Ok(())
Expand Down Expand Up @@ -237,3 +288,81 @@ fn siginfo_to_wait_status(

Ok(status)
}

#[cfg(test)]
mod tests {
#[cfg(target_os = "linux")]
mod linux {
use super::super::{
realtime_signal_definitions, realtime_signal_from_name, realtime_signal_name,
};

#[test]
fn realtime_signal_names_use_runtime_bounds() {
let min = nix::libc::SIGRTMIN();
let max = nix::libc::SIGRTMAX();
let midpoint = min + (max - min) / 2;

assert_eq!(realtime_signal_name(min), Some("SIGRTMIN"));
assert_eq!(realtime_signal_name(max), Some("SIGRTMAX"));
assert_eq!(
realtime_signal_name(midpoint),
Some(format!("SIGRTMIN+{}", midpoint - min).as_str())
);
assert_eq!(
realtime_signal_name(midpoint + 1),
Some(format!("SIGRTMAX-{}", max - midpoint - 1).as_str())
);
}

#[test]
fn realtime_signal_names_and_numbers_are_parsed() {
let min = nix::libc::SIGRTMIN();
let max = nix::libc::SIGRTMAX();
let span = max - min;

assert_eq!(realtime_signal_from_name("SIGRTMIN"), Some(min));
assert_eq!(realtime_signal_from_name("SIGRTMIN+0"), Some(min));
assert_eq!(
realtime_signal_from_name(format!("SIGRTMIN+{span}").as_str()),
Some(max)
);
assert_eq!(realtime_signal_from_name("SIGRTMAX-1"), Some(max - 1));
assert!(realtime_signal_name(min).is_some());
assert!(realtime_signal_name(max).is_some());
}

#[test]
fn invalid_realtime_signal_offsets_are_rejected() {
let min = nix::libc::SIGRTMIN();
let max = nix::libc::SIGRTMAX();
let span = max - min;

assert!(realtime_signal_from_name(format!("SIGRTMIN+{}", span + 1).as_str()).is_none());
assert!(realtime_signal_from_name("SIGRTMAX-0").is_none());
assert!(realtime_signal_from_name(format!("SIGRTMAX-{span}").as_str()).is_none());
assert!(realtime_signal_name(min - 1).is_none());
assert!(realtime_signal_name(max + 1).is_none());
}

#[test]
fn iterator_uses_canonical_realtime_names() {
let min = nix::libc::SIGRTMIN();
let max = nix::libc::SIGRTMAX();
let realtime = realtime_signal_definitions();

assert_eq!(
realtime.len(),
usize::try_from(max - min + 1).unwrap_or_default()
);
assert_eq!(
realtime.first().map(|(_, name)| name.as_str()),
Some("SIGRTMIN")
);
assert_eq!(
realtime.last().map(|(_, name)| name.as_str()),
Some("SIGRTMAX")
);
}
}
}
Loading
Loading