use anyhow::{Context, Result, bail};
use std::io::{Read, Write};
use std::os::fd::{FromRawFd, RawFd};
pub const CHILD_FD: RawFd = 3;
pub const LISTENER_FD: RawFd = 4;
pub const PARENT_FD: RawFd = LISTENER_FD + crate::MAX_LISTENERS as RawFd;
pub const MAX_PASSED: usize = 2 + crate::MAX_LISTENERS;
const _: () = assert!(PARENT_FD < MOVE_FLOOR);
pub const PARENT_GONE_DEADLINE: std::time::Duration = std::time::Duration::from_secs(20);
const MOVE_FLOOR: RawFd = 64;
pub fn pass_to_child(cmd: &mut tokio::process::Command, fds: &[(RawFd, RawFd)]) {
assert!(fds.len() <= MAX_PASSED && fds.iter().all(|&(_, to)| (3..MOVE_FLOOR).contains(&to)));
let fds = fds.to_vec();
unsafe {
cmd.pre_exec(move || {
let mut moved = [0; MAX_PASSED];
for (slot, &(fd, _)) in moved.iter_mut().zip(&fds) {
*slot = libc::fcntl(fd, libc::F_DUPFD_CLOEXEC, MOVE_FLOOR);
if *slot < 0 {
return Err(std::io::Error::last_os_error());
}
}
for (&from, &(_, to)) in moved.iter().zip(&fds) {
if libc::dup2(from, to) < 0 {
return Err(std::io::Error::last_os_error());
}
libc::close(from);
}
Ok(())
});
}
}
pub fn check_pipe(flag: &str, fd: RawFd) -> Result<()> {
if unsafe { libc::fcntl(fd, libc::F_GETFD) } < 0 {
bail!(
"{flag} {fd} is not an open descriptor: {}",
std::io::Error::last_os_error()
);
}
let mut stat: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::fstat(fd, &mut stat) } < 0 {
return Err(std::io::Error::last_os_error()).with_context(|| format!("{flag} {fd}: fstat"));
}
if stat.st_mode & libc::S_IFMT != libc::S_IFIFO {
bail!("{flag} {fd} is not a pipe");
}
Ok(())
}
pub fn write_code(fd: RawFd, code: &str) -> Result<()> {
let mut file = unsafe { std::fs::File::from_raw_fd(fd) };
writeln!(file, "{code}").context("write the pairing code to the supervisor's pipe")?;
Ok(())
}
const MAX_CODE_BYTES: usize = 256;
pub async fn read_code(fd: RawFd) -> Result<String> {
let file = unsafe { std::fs::File::from_raw_fd(fd) };
let text = tokio::task::spawn_blocking(move || {
let mut text = String::new();
file.take(MAX_CODE_BYTES as u64 + 1)
.read_to_string(&mut text)
.map(|_| text)
})
.await?
.context("read the pairing code from the supervisor's pipe")?;
if text.len() > MAX_CODE_BYTES {
bail!("the pairing code on the supervisor's pipe is longer than {MAX_CODE_BYTES} bytes");
}
let code = text.trim().to_string();
if code.is_empty() {
bail!("the collector exited without handing over a pairing code");
}
Ok(code)
}
pub fn watch_parent(fd: RawFd) -> Result<tokio::sync::oneshot::Receiver<()>> {
check_pipe("--parent-fd", fd)?;
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFD);
if flags < 0 || libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) < 0 {
return Err(std::io::Error::last_os_error()).context("make --parent-fd close-on-exec");
}
}
let mut file = unsafe { std::fs::File::from_raw_fd(fd) };
let (gone, receiver) = tokio::sync::oneshot::channel();
std::thread::Builder::new()
.name("parent-watch".into())
.spawn(move || {
let mut buf = [0u8; 64];
loop {
match file.read(&mut buf) {
Ok(0) => break,
Ok(_) => {}
Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
Err(_) => break,
}
}
let _ = gone.send(());
std::thread::sleep(PARENT_GONE_DEADLINE);
let _ = std::io::stderr().write_all(
format!(
"hennery: still running {} s after `hennery up` is gone; exiting\n",
PARENT_GONE_DEADLINE.as_secs()
)
.as_bytes(),
);
unsafe { libc::_exit(1) }
})
.context("start the thread that watches --parent-fd")?;
Ok(receiver)
}
pub async fn parent_gone(watch: Option<tokio::sync::oneshot::Receiver<()>>) {
match watch {
Some(watch) => {
let _ = watch.await;
tracing::warn!("hennery up is gone; stopping");
}
None => std::future::pending().await,
}
}
pub fn close(fd: RawFd) {
drop(unsafe { std::fs::File::from_raw_fd(fd) });
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::fd::IntoRawFd;
#[tokio::test]
async fn a_code_longer_than_the_bound_is_refused() {
let (reader, mut writer) = std::io::pipe().unwrap();
let feed = std::thread::spawn(move || {
let _ = writer.write_all(&vec![b'A'; 1 << 20]);
});
let err = read_code(reader.into_raw_fd()).await.unwrap_err().to_string();
assert!(err.contains("longer than"), "{err}");
feed.join().unwrap();
let (reader, mut writer) = std::io::pipe().unwrap();
writer.write_all(b"ABCD-EFGH\n").unwrap();
drop(writer);
assert_eq!(read_code(reader.into_raw_fd()).await.unwrap(), "ABCD-EFGH");
}
}