use crate::engine::trap::Trap;
#[derive(Clone, Debug, Default)]
pub(crate) struct TrapFd {
#[cfg_attr(not(unix), allow(dead_code))]
fd: Option<i32>,
}
impl TrapFd {
pub(crate) fn from_fd(fd: Option<i32>) -> Self {
Self { fd }
}
#[cfg(target_os = "linux")]
pub(crate) fn is_enabled(&self) -> bool {
self.fd.is_some()
}
#[cfg(target_os = "linux")]
pub(crate) fn is_socket(&self) -> bool {
self.fd.is_some_and(|fd| {
crate::platform::fd::getsockopt_int(fd, libc::SOL_SOCKET, libc::SO_TYPE).is_ok()
})
}
#[cfg(target_os = "linux")]
pub(crate) fn close(&self) {
if let Some(fd) = self.fd {
close_trap_fd(fd);
}
}
#[cfg(unix)]
pub(crate) fn fd(&self) -> Option<i32> {
self.fd
}
#[cfg(unix)]
pub(crate) fn write(&self, trap: &Trap) {
self.write_json(&trap.json_line());
}
#[cfg(unix)]
pub(crate) fn write_json(&self, json: &str) {
let mut line = json.to_owned();
line.push('\n');
if let Some(fd) = self.fd {
#[cfg(target_os = "linux")]
if self.is_socket() {
write_socket_trap_fd(fd, line.as_bytes());
return;
}
write_nonblocking_trap_fd(fd, line.as_bytes());
}
}
#[cfg(not(unix))]
#[allow(clippy::unused_self)]
pub(crate) fn write(&self, _trap: &Trap) {}
}
#[cfg(target_os = "linux")]
fn write_socket_trap_fd(fd: i32, line: &[u8]) {
let written = unsafe {
libc::send(
fd,
line.as_ptr().cast(),
line.len(),
libc::MSG_DONTWAIT | libc::MSG_NOSIGNAL,
)
};
log_short_write(fd, line.len(), written);
}
#[cfg(unix)]
fn write_nonblocking_trap_fd(fd: i32, line: &[u8]) {
if line.len() > libc::PIPE_BUF {
log::debug!("trap: dropping fd={fd} record larger than PIPE_BUF");
return;
}
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
if flags < 0 {
log_fd_error("get flags", fd);
return;
}
let restore_flags = flags & libc::O_NONBLOCK == 0;
if restore_flags {
let result = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
if result < 0 {
log_fd_error("set nonblocking", fd);
return;
}
}
let written = write_without_sigpipe(fd, line);
log_short_write(fd, line.len(), written);
if restore_flags {
if unsafe { libc::fcntl(fd, libc::F_SETFL, flags) } < 0 {
log_fd_error("restore flags", fd);
}
}
}
#[cfg(unix)]
fn write_without_sigpipe(fd: i32, line: &[u8]) -> isize {
unsafe {
let mut sigpipe = std::mem::zeroed::<libc::sigset_t>();
if libc::sigemptyset(&mut sigpipe) != 0 || libc::sigaddset(&mut sigpipe, libc::SIGPIPE) != 0
{
return -1;
}
let mut pending = std::mem::zeroed::<libc::sigset_t>();
let was_pending =
libc::sigpending(&mut pending) == 0 && libc::sigismember(&pending, libc::SIGPIPE) == 1;
let mut old_mask = std::mem::zeroed::<libc::sigset_t>();
if libc::pthread_sigmask(libc::SIG_BLOCK, &sigpipe, &mut old_mask) != 0 {
return -1;
}
let written = libc::write(fd, line.as_ptr().cast(), line.len());
let broken_pipe =
written < 0 && std::io::Error::last_os_error().raw_os_error() == Some(libc::EPIPE);
if broken_pipe && !was_pending {
let mut pending = std::mem::zeroed::<libc::sigset_t>();
if libc::sigpending(&mut pending) == 0
&& libc::sigismember(&pending, libc::SIGPIPE) == 1
{
let mut signal = 0;
libc::sigwait(&sigpipe, &mut signal);
}
}
libc::pthread_sigmask(libc::SIG_SETMASK, &old_mask, std::ptr::null_mut());
written
}
}
#[cfg(unix)]
fn log_short_write(fd: i32, expected: usize, written: isize) {
if written == isize::try_from(expected).unwrap_or(-1) {
return;
}
let error = std::io::Error::last_os_error();
log::debug!(
"trap: write fd={fd} bytes={written} errno={}",
error.raw_os_error().unwrap_or(0)
);
}
#[cfg(unix)]
fn log_fd_error(operation: &str, fd: i32) {
let error = std::io::Error::last_os_error();
log::debug!(
"trap: {operation} fd={fd} errno={}",
error.raw_os_error().unwrap_or(0)
);
}
#[cfg(target_os = "linux")]
fn close_trap_fd(fd: i32) {
let rc = unsafe { libc::close(fd) };
if rc != 0 {
let error = std::io::Error::last_os_error();
log::debug!(
"trap: close fd={fd} errno={}",
error.raw_os_error().unwrap_or(0)
);
}
}