use std::path::PathBuf;
use core::sync::atomic::{AtomicBool, Ordering};
use crate::ChannelError;
pub const FD_VAR: &str = "SHEP_CHANNEL_FD";
pub const PIPE_VAR: &str = "SHEP_CHANNEL_PIPE";
pub const VERSION_VAR: &str = "SHEP_CHANNEL_VERSION";
const FIRST_INHERITABLE_FD: i32 = 3;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Endpoint {
Descriptor(i32),
Pipe(PathBuf),
Absent,
}
pub fn discover() -> Result<Endpoint, ChannelError> {
if let Some(raw) = std::env::var_os(FD_VAR) {
return descriptor_from(&raw.to_string_lossy());
}
if let Some(raw) = std::env::var_os(PIPE_VAR) {
return Ok(Endpoint::Pipe(PathBuf::from(raw)));
}
Ok(Endpoint::Absent)
}
fn descriptor_from(text: &str) -> Result<Endpoint, ChannelError> {
let Ok(fd) = text.trim().parse::<i32>() else {
return Err(ChannelError::Unusable(format!("{FD_VAR}={text}")));
};
if fd < FIRST_INHERITABLE_FD {
return Err(ChannelError::Unusable(format!(
"{FD_VAR}={fd} must be {FIRST_INHERITABLE_FD} or above: the shepherd passes \
the channel as {FIRST_INHERITABLE_FD}, and 0, 1 and 2 are this process's own \
standard streams"
)));
}
Ok(Endpoint::Descriptor(fd))
}
#[cfg(unix)]
pub(crate) type Transport = std::os::unix::net::UnixStream;
#[cfg(windows)]
pub(crate) type Transport = std::fs::File;
#[cfg(unix)]
pub(crate) type ReadHalf = Transport;
#[cfg(windows)]
pub(crate) type ReadHalf = PipeReader;
#[cfg(windows)]
const PIPE_POLL_INTERVAL: core::time::Duration = core::time::Duration::from_millis(20);
#[cfg(windows)]
#[derive(Debug)]
pub(crate) struct PipeReader {
pipe: std::fs::File,
}
#[cfg(windows)]
impl PipeReader {
fn buffered(&self) -> std::io::Result<Option<u32>> {
use std::os::windows::io::AsRawHandle as _;
use windows_sys::Win32::Foundation::{ERROR_BROKEN_PIPE, ERROR_PIPE_NOT_CONNECTED};
use windows_sys::Win32::System::Pipes::PeekNamedPipe;
let mut available: u32 = 0;
#[allow(unsafe_code)]
let reported = unsafe {
PeekNamedPipe(
self.pipe.as_raw_handle(),
core::ptr::null_mut(),
0,
core::ptr::null_mut(),
&raw mut available,
core::ptr::null_mut(),
)
};
if reported == 0 {
let error = std::io::Error::last_os_error();
let ended = matches!(
error.raw_os_error(),
Some(code)
if code == ERROR_BROKEN_PIPE as i32
|| code == ERROR_PIPE_NOT_CONNECTED as i32
);
return if ended { Ok(None) } else { Err(error) };
}
Ok(Some(available))
}
}
#[cfg(windows)]
impl std::io::Read for PipeReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if buf.is_empty() {
return Ok(0);
}
loop {
let Some(buffered) = self.buffered()? else {
return Ok(0);
};
if buffered == 0 {
std::thread::sleep(PIPE_POLL_INTERVAL);
continue;
}
let want = buf.len().min(buffered as usize);
return self.pipe.read(&mut buf[..want]);
}
}
}
#[cfg(unix)]
fn read_half(transport: Transport) -> ReadHalf {
transport
}
#[cfg(windows)]
fn read_half(transport: Transport) -> ReadHalf {
PipeReader { pipe: transport }
}
static CHANNEL_TAKEN: AtomicBool = AtomicBool::new(false);
pub(crate) fn connect(endpoint: &Endpoint) -> Result<(ReadHalf, Transport), ChannelError> {
let transport = match endpoint {
#[cfg(unix)]
Endpoint::Descriptor(fd) => {
if CHANNEL_TAKEN.swap(true, Ordering::SeqCst) {
return Err(ChannelError::AlreadyTaken);
}
use std::os::fd::FromRawFd as _;
#[allow(unsafe_code)]
unsafe {
Transport::from_raw_fd(*fd)
}
}
#[cfg(windows)]
Endpoint::Pipe(path) => {
let opened = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path)
.map_err(ChannelError::Io)?;
if CHANNEL_TAKEN.swap(true, Ordering::SeqCst) {
drop(opened);
return Err(ChannelError::AlreadyTaken);
}
opened
}
#[cfg(unix)]
Endpoint::Pipe(path) => {
return Err(ChannelError::Unusable(format!(
"{PIPE_VAR}={} names a Windows named pipe and this is not Windows",
path.display()
)));
}
#[cfg(windows)]
Endpoint::Descriptor(fd) => {
return Err(ChannelError::Unusable(format!(
"{FD_VAR}={fd} names an inherited descriptor and Windows does not inherit one"
)));
}
Endpoint::Absent => {
return Err(ChannelError::Unusable(
"no channel: neither variable is set".to_string(),
));
}
};
let writer = match transport.try_clone() {
Ok(writer) => writer,
Err(error) => {
#[cfg(windows)]
{
drop(transport);
CHANNEL_TAKEN.store(false, Ordering::SeqCst);
}
return Err(ChannelError::Io(error));
}
};
Ok((read_half(transport), writer))
}
#[cfg(test)]
mod tests {
#[cfg(unix)]
use std::os::fd::IntoRawFd as _;
#[cfg(unix)]
use std::os::unix::net::UnixStream;
use super::*;
#[test]
fn the_pipe_endpoint_prints_its_path_in_full() {
let endpoint = Endpoint::Pipe(PathBuf::from(
r"\\.\pipe\shep-channel-1234-0-0123456789abcdef0123456789abcdef",
));
assert_eq!(
format!("{endpoint:?}"),
r#"Pipe("\\\\.\\pipe\\shep-channel-1234-0-0123456789abcdef0123456789abcdef")"#
);
}
#[test]
fn the_other_endpoints_print_only_what_they_hold() {
assert_eq!(format!("{:?}", Endpoint::Descriptor(3)), "Descriptor(3)");
assert_eq!(format!("{:?}", Endpoint::Absent), "Absent");
}
#[test]
fn a_negative_descriptor_is_refused() {
match descriptor_from("-1") {
Err(ChannelError::Unusable(what)) => assert!(
what.contains(&format!("{FD_VAR}=-1")),
"the refusal names neither the variable nor the value: {what}"
),
other => panic!("expected a refusal, got {other:?}"),
}
}
#[test]
fn stdout_is_refused_as_a_descriptor() {
match descriptor_from("1") {
Err(ChannelError::Unusable(what)) => assert!(
what.contains(&format!("{FD_VAR}=1")),
"the refusal names neither the variable nor the value: {what}"
),
other => panic!("expected a refusal, got {other:?}"),
}
}
#[test]
fn the_descriptor_the_shepherd_passes_is_accepted() {
assert!(matches!(
descriptor_from(" 3 "),
Ok(Endpoint::Descriptor(FIRST_INHERITABLE_FD))
));
}
#[cfg(unix)]
#[test]
fn a_descriptor_can_only_be_taken_once() {
let (ours, _theirs) = UnixStream::pair().expect("socketpair");
let fd = ours.into_raw_fd();
let endpoint = Endpoint::Descriptor(fd);
let first = connect(&endpoint);
assert!(first.is_ok(), "first take should succeed: {first:?}");
let second = connect(&endpoint);
assert!(matches!(second, Err(ChannelError::AlreadyTaken)));
drop(first);
}
}