use std::fs; use std::io::{self, Read}; #[cfg(unix)] use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::path::Path; #[cfg(unix)] use interprocess::local_socket::traits::Stream as _; pub(crate) type LocalListener = interprocess::local_socket::Listener; pub(crate) type LocalStream = interprocess::local_socket::Stream; pub(crate) enum LocalStreamRead { Data, Pending, Closed, } #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct SocketFileIdentity { #[cfg(unix)] dev: u64, #[cfg(unix)] ino: u64, #[cfg(windows)] marker: Vec, } pub(crate) fn connect_local_stream(path: &Path) -> io::Result { #[cfg(unix)] { use interprocess::local_socket::{prelude::*, GenericFilePath}; let name = path.to_fs_name::()?; LocalStream::connect(name) } #[cfg(windows)] { use interprocess::local_socket::{prelude::*, GenericNamespaced}; let name = path.to_string_lossy().to_string(); let name = name.to_ns_name::()?; LocalStream::connect(name) } } pub(crate) fn bind_local_listener(path: &Path) -> io::Result { #[cfg(unix)] { use interprocess::local_socket::{prelude::*, GenericFilePath, ListenerOptions}; let name = path.to_fs_name::()?; ListenerOptions::new() .name(name) .reclaim_name(false) .create_sync() } #[cfg(windows)] { use interprocess::local_socket::{prelude::*, GenericNamespaced, ListenerOptions}; let name = path.to_string_lossy().to_string(); let name = name.to_ns_name::()?; let listener = ListenerOptions::new() .name(name) .reclaim_name(false) .create_sync()?; fs::write(path, windows_socket_marker())?; Ok(listener) } } pub(crate) fn prepare_socket_path( path: &Path, busy_message: impl FnOnce(&Path) -> String, ) -> io::Result<()> { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } if !path.exists() { return Ok(()); } match connect_local_stream(path) { Ok(_) => { return Err(io::Error::new(io::ErrorKind::AddrInUse, busy_message(path))); } Err(err) if stale_socket_connect_error(err.kind()) => {} Err(err) => return Err(err), } if let Err(err) = fs::remove_file(path) { if err.kind() != io::ErrorKind::NotFound { return Err(err); } } Ok(()) } fn stale_socket_connect_error(kind: io::ErrorKind) -> bool { matches!( kind, io::ErrorKind::ConnectionRefused | io::ErrorKind::NotFound | io::ErrorKind::TimedOut ) || (cfg!(windows) && kind == io::ErrorKind::WouldBlock) } pub(crate) fn local_stream_peer_closed(stream: &mut LocalStream) -> io::Result { probe_stream_closed(stream) } pub(crate) fn set_local_stream_polling(stream: &mut LocalStream, enabled: bool) -> io::Result<()> { #[cfg(unix)] { stream.set_nonblocking(enabled) } #[cfg(windows)] { let _ = (stream, enabled); Ok(()) } } pub(crate) fn poll_local_stream_read( stream: &mut LocalStream, buf: &mut [u8], ) -> io::Result { #[cfg(unix)] { match stream.read(buf) { Ok(0) => Ok(LocalStreamRead::Closed), Ok(_) => Ok(LocalStreamRead::Data), Err(err) if err.kind() == io::ErrorKind::WouldBlock => Ok(LocalStreamRead::Pending), Err(err) => Err(err), } } #[cfg(windows)] { match windows_named_pipe_available(stream)? { None => Ok(LocalStreamRead::Closed), Some(0) => Ok(LocalStreamRead::Pending), Some(_) => match stream.read(buf) { Ok(0) => Ok(LocalStreamRead::Closed), Ok(_) => Ok(LocalStreamRead::Data), Err(err) if is_connection_closed_error(&err) => Ok(LocalStreamRead::Closed), Err(err) => Err(err), }, } } } #[cfg(unix)] fn probe_stream_closed(stream: &mut LocalStream) -> io::Result { stream.set_nonblocking(true)?; let mut probe = [0u8; 1]; let status = match stream.read(&mut probe) { Ok(0) => Ok(true), Ok(_) => Ok(true), Err(err) if matches!( err.kind(), io::ErrorKind::WouldBlock | io::ErrorKind::Interrupted ) => { Ok(false) } Err(err) if is_connection_closed_error(&err) => Ok(true), Err(err) => Err(err), }; stream.set_nonblocking(false)?; status } #[cfg(windows)] fn probe_stream_closed(stream: &mut LocalStream) -> io::Result { Ok(windows_named_pipe_available(stream)?.is_none()) } #[cfg(windows)] fn windows_named_pipe_available(stream: &mut LocalStream) -> io::Result> { use std::os::windows::io::{AsHandle, AsRawHandle}; let LocalStream::NamedPipe(pipe) = stream; let mut available = 0; let ok = unsafe { windows_sys::Win32::System::Pipes::PeekNamedPipe( pipe.as_handle().as_raw_handle(), std::ptr::null_mut(), 0, std::ptr::null_mut(), &mut available, std::ptr::null_mut(), ) }; if ok != 0 { return Ok(Some(available)); } let err = io::Error::last_os_error(); if is_connection_closed_error(&err) || windows_named_pipe_closed_error(&err) { return Ok(None); } Err(err) } pub(crate) fn is_connection_closed_error(err: &io::Error) -> bool { matches!( err.kind(), io::ErrorKind::BrokenPipe | io::ErrorKind::ConnectionAborted | io::ErrorKind::ConnectionReset | io::ErrorKind::NotConnected | io::ErrorKind::UnexpectedEof | io::ErrorKind::WriteZero ) } #[cfg(windows)] fn windows_named_pipe_closed_error(err: &io::Error) -> bool { matches!(err.raw_os_error(), Some(6 | 109 | 232 | 233)) } pub(crate) fn socket_file_identity(path: &Path) -> io::Result { #[cfg(windows)] { Ok(SocketFileIdentity { marker: fs::read(path)?, }) } #[cfg(unix)] { let metadata = fs::metadata(path)?; Ok(SocketFileIdentity { dev: metadata.dev(), ino: metadata.ino(), }) } } pub(crate) fn remove_socket_file_if_owned( path: &Path, identity: &SocketFileIdentity, ) -> io::Result<()> { let current = match socket_file_identity(path) { Ok(current) => current, Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()), Err(err) => return Err(err), }; if current != *identity { return Ok(()); } match fs::remove_file(path) { Ok(()) => Ok(()), Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()), Err(err) => Err(err), } } #[cfg(windows)] fn windows_socket_marker() -> String { let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|duration| duration.as_nanos()) .unwrap_or(0); format!("{}:{now}", std::process::id()) } #[cfg(unix)] pub(crate) fn restrict_socket_permissions(path: &Path, mode: u32) -> io::Result<()> { let mut permissions = fs::metadata(path)?.permissions(); permissions.set_mode(mode); fs::set_permissions(path, permissions) } #[cfg(windows)] pub(crate) fn restrict_socket_permissions(_path: &Path, _mode: u32) -> io::Result<()> { Ok(()) } #[cfg(test)] mod tests { use super::*; #[cfg(windows)] use interprocess::local_socket::traits::Listener as _; #[cfg(windows)] use std::path::PathBuf; #[test] fn stale_socket_connect_errors_keep_unix_would_block_strict() { assert!(stale_socket_connect_error(io::ErrorKind::ConnectionRefused)); assert!(stale_socket_connect_error(io::ErrorKind::NotFound)); assert!(stale_socket_connect_error(io::ErrorKind::TimedOut)); assert_eq!( stale_socket_connect_error(io::ErrorKind::WouldBlock), cfg!(windows) ); } #[cfg(windows)] #[test] fn remove_socket_file_if_owned_compares_windows_marker_contents() { let path = temp_socket_marker_path("same-len-marker"); let _ = fs::remove_file(&path); fs::write(&path, b"marker-aa").expect("write first marker"); let identity = socket_file_identity(&path).expect("read first identity"); fs::write(&path, b"marker-bb").expect("replace with same-length marker"); remove_socket_file_if_owned(&path, &identity).expect("remove owned marker"); assert!(path.exists(), "same-length replacement marker must survive"); let _ = fs::remove_file(&path); } #[cfg(windows)] #[test] fn idle_named_pipe_peer_is_not_treated_as_closed() { let path = temp_socket_marker_path("idle-pipe"); let listener = bind_local_listener(&path).unwrap(); let _client = connect_local_stream(&path).unwrap(); let mut server = listener.accept().unwrap(); assert!(!local_stream_peer_closed(&mut server).unwrap()); let _ = fs::remove_file(path); } #[cfg(windows)] #[test] fn disconnected_named_pipe_peer_is_treated_as_closed() { let path = temp_socket_marker_path("disconnected-pipe"); let listener = bind_local_listener(&path).unwrap(); let client = connect_local_stream(&path).unwrap(); let mut server = listener.accept().unwrap(); drop(client); assert!(local_stream_peer_closed(&mut server).unwrap()); let _ = fs::remove_file(path); } #[cfg(windows)] fn temp_socket_marker_path(name: &str) -> PathBuf { std::env::temp_dir().join(format!("herdr-{name}-{}.sock", std::process::id())) } }