use std::{
io,
os::fd::RawFd,
sync::{Mutex, TryLockError},
time::{Duration, Instant},
};
use crate::{ProcessGroupId, Signal};
use super::{MAX_POLL_INTERVAL, child_kill_group, wait_ready};
#[cfg(test)]
mod tests;
pub(super) const SIGNAL_RECORD: u8 = 0xa6;
const REQUEST_BYTES: usize = 9;
const REPLY_BYTES: usize = 12;
const PIN_LOST: &str = "process-group helper is gone; the pin may be lost and the group must not be signalled directly";
#[derive(Debug, Default)]
pub(super) struct Exchange {
last_id: u32,
reply: [u8; REPLY_BYTES],
filled: usize,
}
impl Exchange {
fn next_id(&mut self) -> io::Result<u32> {
self.last_id = self.last_id.checked_add(1).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"guard signal request IDs exhausted",
)
})?;
Ok(self.last_id)
}
}
pub(super) fn signal_group(
descriptor: RawFd,
exchange: &Mutex<Exchange>,
signal: Signal,
deadline: Instant,
) -> io::Result<()> {
let mut state = loop {
match exchange.try_lock() {
Ok(lock) => break lock,
Err(TryLockError::Poisoned(_)) => {
return Err(io::Error::other("guard signal exchange lock is poisoned"));
}
Err(TryLockError::WouldBlock) => pause(deadline)?,
}
};
let id = state.next_id()?;
let mut request = [0_u8; REQUEST_BYTES];
request[0] = SIGNAL_RECORD;
request[1..5].copy_from_slice(&id.to_ne_bytes());
request[5..].copy_from_slice(&signal.get().to_ne_bytes());
loop {
wait_ready(descriptor, libc::POLLOUT, deadline)?;
let sent = unsafe {
libc::send(
descriptor,
request.as_ptr().cast(),
request.len(),
libc::MSG_NOSIGNAL | libc::MSG_DONTWAIT,
)
};
if usize::try_from(sent) == Ok(REQUEST_BYTES) {
break;
}
if sent >= 0 {
unsafe { libc::shutdown(descriptor, libc::SHUT_WR) };
return Err(io::Error::new(io::ErrorKind::BrokenPipe, PIN_LOST));
}
let error = socket_error();
if !matches!(
error.kind(),
io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock
) {
return Err(error);
}
}
receive_reply(descriptor, &mut state, id, signal, deadline)
}
fn receive_reply(
descriptor: RawFd,
state: &mut Exchange,
id: u32,
signal: Signal,
deadline: Instant,
) -> io::Result<()> {
loop {
wait_ready(descriptor, libc::POLLIN, deadline)?;
let remaining = state.reply.get_mut(state.filled..).ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "invalid guard reply offset")
})?;
let read = unsafe {
libc::recv(
descriptor,
remaining.as_mut_ptr().cast(),
remaining.len(),
libc::MSG_DONTWAIT,
)
};
match usize::try_from(read) {
Ok(0) => return Err(io::Error::new(io::ErrorKind::UnexpectedEof, PIN_LOST)),
Ok(count) => state.filled += count,
Err(_) => {
let error = socket_error();
if !matches!(
error.kind(),
io::ErrorKind::Interrupted | io::ErrorKind::WouldBlock
) {
return Err(error);
}
continue;
}
}
if state.filled == REPLY_BYTES {
let [r0, r1, r2, r3, s0, s1, s2, s3, e0, e1, e2, e3] = state.reply;
let (request, signal_bytes, error_bytes) =
([r0, r1, r2, r3], [s0, s1, s2, s3], [e0, e1, e2, e3]);
state.filled = 0;
if u32::from_ne_bytes(request) != id {
continue;
}
if i32::from_ne_bytes(signal_bytes) != signal.get() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"guard reply signal mismatch",
));
}
let errno = i32::from_ne_bytes(error_bytes);
return if errno == 0 {
Ok(())
} else {
Err(io::Error::from_raw_os_error(errno))
};
}
}
}
fn pause(deadline: Instant) -> io::Result<()> {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"timed out waiting for process-group helper",
));
}
std::thread::sleep(
remaining
.min(MAX_POLL_INTERVAL)
.min(Duration::from_millis(1)),
);
Ok(())
}
fn socket_error() -> io::Error {
let error = io::Error::last_os_error();
if matches!(error.raw_os_error(), Some(libc::EPIPE | libc::ECONNRESET)) {
io::Error::new(io::ErrorKind::BrokenPipe, PIN_LOST)
} else {
error
}
}
pub(super) fn child_signal(descriptor: RawFd, group: ProcessGroupId) {
let mut record = [0_u8; 8];
let mut filled = 0;
while filled < record.len() {
let Some(remaining) = record.get_mut(filled..) else {
child_kill_group(group);
};
let read =
unsafe { libc::read(descriptor, remaining.as_mut_ptr().cast(), remaining.len()) };
if read > 0 {
filled += read.cast_unsigned();
} else if !(read == -1 && crate::raw::last_errno() == libc::EINTR) {
child_kill_group(group);
}
}
let [_, _, _, _, a, b, c, d] = record;
let signal = i32::from_ne_bytes([a, b, c, d]);
if signal <= 0 {
child_kill_group(group);
}
let errno = loop {
if unsafe { libc::kill(-group.get(), signal) } == 0 {
break 0_i32;
}
let errno = crate::raw::last_errno();
if errno != libc::EINTR {
break errno;
}
};
let mut reply = [0_u8; REPLY_BYTES];
reply[..8].copy_from_slice(&record);
reply[8..].copy_from_slice(&errno.to_ne_bytes());
let mut sent = 0;
while sent < reply.len() {
let Some(remaining) = reply.get(sent..) else {
return;
};
let written = unsafe {
libc::send(
descriptor,
remaining.as_ptr().cast(),
remaining.len(),
libc::MSG_NOSIGNAL,
)
};
if written > 0 {
sent += written.cast_unsigned();
} else if !(written == -1 && crate::raw::last_errno() == libc::EINTR) {
return;
}
}
}