use std::{io, os::fd::RawFd};
const SPARE_DESCRIPTORS: usize = 64;
#[derive(Clone, Copy)]
struct PipeEnd {
fd: RawFd,
end: Key,
peer: Key,
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Key(u64, u64);
pub(super) struct Inherited {
ends: Vec<PipeEnd>,
#[cfg(target_os = "macos")]
listed: Vec<libc::proc_fdinfo>,
}
impl Inherited {
pub(super) fn reserve() -> Self {
let capacity = open_descriptors()
.unwrap_or(0)
.saturating_mul(2)
.saturating_add(SPARE_DESCRIPTORS);
Self {
ends: Vec::with_capacity(capacity),
#[cfg(target_os = "macos")]
listed: Vec::with_capacity(capacity),
}
}
pub(super) fn release(&mut self, keep: RawFd) {
self.ends.clear();
self.collect(keep);
self.ends.sort_unstable_by_key(|end| end.end);
for end in &self.ends {
if self
.ends
.binary_search_by_key(&end.peer, |other| other.end)
.is_ok()
{
unsafe { libc::close(end.fd) };
}
}
}
fn push(&mut self, end: PipeEnd) {
if self.ends.len() < self.ends.capacity() {
self.ends.push(end);
}
}
}
fn inherited(fd: RawFd, keep: RawFd) -> bool {
if fd <= libc::STDERR_FILENO || fd == keep {
return false;
}
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
flags >= 0 && flags & libc::FD_CLOEXEC != 0
}
#[cfg(not(target_os = "macos"))]
fn open_descriptors() -> io::Result<usize> {
Ok(std::fs::read_dir("/proc/self/fd")?.count())
}
#[cfg(not(target_os = "macos"))]
const RECORD_LENGTH_OFFSET: usize = 16;
#[cfg(not(target_os = "macos"))]
const NAME_OFFSET: usize = 19;
#[cfg(not(target_os = "macos"))]
const DIRECTORY_BUFFER_BYTES: usize = 4096;
#[cfg(not(target_os = "macos"))]
impl Inherited {
fn collect(&mut self, keep: RawFd) {
let directory = unsafe {
libc::open(
c"/proc/self/fd".as_ptr(),
libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC,
)
};
if directory < 0 {
return;
}
let mut buffer = [0_u8; DIRECTORY_BUFFER_BYTES];
loop {
let read = unsafe {
libc::syscall(
libc::SYS_getdents64,
directory,
buffer.as_mut_ptr(),
buffer.len(),
)
};
let Ok(read) = usize::try_from(read) else {
break;
};
if read == 0 {
break;
}
let mut records = buffer.get(..read).unwrap_or_default();
while let Some(&[low, high]) = records.get(RECORD_LENGTH_OFFSET..NAME_OFFSET - 1) {
let length = usize::from(u16::from_ne_bytes([low, high]));
let (Some(name), Some(rest)) =
(records.get(NAME_OFFSET..length), records.get(length..))
else {
break;
};
if let Some(fd) = descriptor(name)
&& fd != directory
&& inherited(fd, keep)
&& let Some(end) = pipe_end(fd)
{
self.push(end);
}
records = rest;
}
}
unsafe { libc::close(directory) };
}
}
#[cfg(not(target_os = "macos"))]
fn descriptor(name: &[u8]) -> Option<RawFd> {
let digits = name.split(|&byte| byte == 0).next()?;
if digits.is_empty() {
return None;
}
digits.iter().try_fold(0 as RawFd, |fd, &byte| {
let digit = RawFd::from(byte.checked_sub(b'0').filter(|digit| *digit <= 9)?);
fd.checked_mul(10)?.checked_add(digit)
})
}
#[cfg(not(target_os = "macos"))]
fn pipe_end(fd: RawFd) -> Option<PipeEnd> {
let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
let stat = unsafe {
if libc::fstat(fd, stat.as_mut_ptr()) != 0 {
return None;
}
stat.assume_init()
};
if stat.st_mode & libc::S_IFMT != libc::S_IFIFO {
return None;
}
let mode = unsafe { libc::fcntl(fd, libc::F_GETFL) };
let writes = match mode {
..0 => return None,
mode if mode & libc::O_ACCMODE == libc::O_RDONLY => 0,
mode if mode & libc::O_ACCMODE == libc::O_WRONLY => 1,
_ => return None,
};
let inode = stat.st_ino;
Some(PipeEnd {
fd,
end: Key(inode, writes),
peer: Key(inode, 1 - writes),
})
}
#[cfg(target_os = "macos")]
#[repr(C)]
struct ProcFileInfo {
open_flags: u32,
status: u32,
offset: libc::off_t,
kind: i32,
guard_flags: u32,
}
#[cfg(target_os = "macos")]
#[repr(C)]
struct PipeInfo {
stat: libc::vinfo_stat,
handle: u64,
peer_handle: u64,
status: i32,
reserved: i32,
}
#[cfg(target_os = "macos")]
#[repr(C)]
struct PipeFdInfo {
file: ProcFileInfo,
pipe: PipeInfo,
}
#[cfg(target_os = "macos")]
const PROC_PIDFDPIPEINFO: libc::c_int = 6;
#[cfg(target_os = "macos")]
fn open_descriptors() -> io::Result<usize> {
let pid = own_pid().ok_or_else(|| io::Error::other("the process ID exceeds pid_t"))?;
let bytes =
unsafe { libc::proc_pidinfo(pid, libc::PROC_PIDLISTFDS, 0, std::ptr::null_mut(), 0) };
let bytes = usize::try_from(bytes).map_err(|_| io::Error::last_os_error())?;
Ok(bytes / std::mem::size_of::<libc::proc_fdinfo>())
}
#[cfg(target_os = "macos")]
impl Inherited {
fn collect(&mut self, keep: RawFd) {
let Some(pid) = own_pid() else {
return;
};
self.listed.clear();
let room = self.listed.capacity() * std::mem::size_of::<libc::proc_fdinfo>();
let Ok(room) = libc::c_int::try_from(room) else {
return;
};
let written = unsafe {
libc::proc_pidinfo(
pid,
libc::PROC_PIDLISTFDS,
0,
self.listed.as_mut_ptr().cast(),
room,
)
};
let Ok(written) = usize::try_from(written) else {
return;
};
let count =
(written / std::mem::size_of::<libc::proc_fdinfo>()).min(self.listed.capacity());
unsafe { self.listed.set_len(count) };
for index in 0..count {
let listed = &self.listed[index];
let fd = listed.proc_fd;
if listed.proc_fdtype == libc::PROX_FDTYPE_PIPE as u32
&& inherited(fd, keep)
&& let Some(end) = pipe_end(pid, fd)
{
self.push(end);
}
}
}
}
#[cfg(target_os = "macos")]
fn own_pid() -> Option<libc::pid_t> {
libc::pid_t::try_from(std::process::id()).ok()
}
#[cfg(target_os = "macos")]
fn pipe_end(pid: libc::pid_t, fd: RawFd) -> Option<PipeEnd> {
let mut info = std::mem::MaybeUninit::<PipeFdInfo>::uninit();
let size = libc::c_int::try_from(std::mem::size_of::<PipeFdInfo>()).ok()?;
let info = unsafe {
if libc::proc_pidfdinfo(pid, fd, PROC_PIDFDPIPEINFO, info.as_mut_ptr().cast(), size) != size
{
return None;
}
info.assume_init()
};
if info.pipe.peer_handle == 0 {
return None;
}
Some(PipeEnd {
fd,
end: Key(info.pipe.handle, 0),
peer: Key(info.pipe.peer_handle, 0),
})
}