use std::io;
use std::path::{Path, PathBuf};
#[cfg(unix)]
use std::time::{Duration, Instant};
#[cfg(unix)]
pub use unix_impl::{connect, peer_is_same_user, DaemonListener, DaemonStream};
#[cfg(windows)]
pub use windows_impl::{connect, peer_is_same_user, pipe_name, DaemonListener, DaemonStream};
#[cfg_attr(not(unix), allow(clippy::unused_async))] pub async fn restrict_to_owner(path: &Path, mode: u32) -> io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(mode)).await
}
#[cfg(not(unix))]
{
let _ = (path, mode);
Ok(())
}
}
pub fn lock_path(socket: &Path) -> PathBuf {
let mut name = socket.file_name().map(std::ffi::OsStr::to_os_string).unwrap_or_default();
name.push(".lock");
socket.with_file_name(name)
}
#[cfg(unix)]
const CLAIM_RETRY_WINDOW: Duration = Duration::from_millis(250);
#[cfg(unix)]
async fn claim_lock(path: &Path) -> io::Result<std::fs::File> {
use std::os::unix::fs::OpenOptionsExt as _;
let deadline = Instant::now() + CLAIM_RETRY_WINDOW;
loop {
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.mode(0o600)
.open(path)?;
if crate::os::unix::try_lock_exclusive(&file)? {
return Ok(file);
}
if Instant::now() >= deadline {
return Err(io::Error::new(
io::ErrorKind::AddrInUse,
format!("another daemon is starting or listening (lock {})", path.display()),
));
}
drop(file);
tokio::time::sleep(Duration::from_millis(10)).await;
}
}
#[cfg(unix)]
mod unix_impl {
use std::io;
use std::path::Path;
use tokio::net::{UnixListener, UnixStream};
pub type DaemonStream = UnixStream;
pub struct DaemonListener {
inner: UnixListener,
_lock: std::fs::File,
}
impl DaemonListener {
pub async fn bind(path: &Path) -> io::Result<Self> {
let lock = super::claim_lock(&super::lock_path(path)).await?;
if tokio::fs::try_exists(path).await? {
if connect(path).await.is_ok() {
return Err(io::Error::new(
io::ErrorKind::AddrInUse,
format!("a daemon is already listening at {}", path.display()),
));
}
tokio::fs::remove_file(path).await?;
}
Ok(Self { inner: UnixListener::bind(path)?, _lock: lock })
}
pub async fn accept(&mut self) -> io::Result<DaemonStream> {
self.inner.accept().await.map(|(stream, _)| stream)
}
}
pub async fn connect(path: &Path) -> io::Result<DaemonStream> {
UnixStream::connect(path).await
}
pub fn peer_is_same_user(stream: &DaemonStream) -> io::Result<bool> {
Ok(stream.peer_cred()?.uid() == crate::os::unix::effective_uid())
}
}
#[cfg(windows)]
mod windows_impl {
use std::io;
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use sha2::{Digest as _, Sha256};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio::net::windows::named_pipe::{
ClientOptions, NamedPipeClient, NamedPipeServer, ServerOptions,
};
use windows_sys::Win32::Foundation::ERROR_PIPE_BUSY;
use crate::os::windows::PipeSecurity;
const PIPE_BUSY_RETRY_WINDOW: Duration = Duration::from_secs(2);
pub fn pipe_name(path: &Path) -> String {
let normalized = path.to_string_lossy().to_lowercase();
let digest = Sha256::digest(normalized.as_bytes());
let mut tag = String::with_capacity(24);
for byte in &digest[..12] {
use std::fmt::Write as _;
let _ = write!(tag, "{byte:02x}");
}
format!(r"\\.\pipe\winx-{tag}")
}
#[derive(Debug)]
pub enum DaemonStream {
Server(NamedPipeServer),
Client(NamedPipeClient),
}
impl AsyncRead for DaemonStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
match self.get_mut() {
Self::Server(pipe) => Pin::new(pipe).poll_read(cx, buf),
Self::Client(pipe) => Pin::new(pipe).poll_read(cx, buf),
}
}
}
impl AsyncWrite for DaemonStream {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
data: &[u8],
) -> Poll<io::Result<usize>> {
match self.get_mut() {
Self::Server(pipe) => Pin::new(pipe).poll_write(cx, data),
Self::Client(pipe) => Pin::new(pipe).poll_write(cx, data),
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match self.get_mut() {
Self::Server(pipe) => Pin::new(pipe).poll_flush(cx),
Self::Client(pipe) => Pin::new(pipe).poll_flush(cx),
}
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match self.get_mut() {
Self::Server(pipe) => Pin::new(pipe).poll_shutdown(cx),
Self::Client(pipe) => Pin::new(pipe).poll_shutdown(cx),
}
}
}
impl DaemonStream {
pub fn try_read(&self, buf: &mut [u8]) -> io::Result<usize> {
match self {
Self::Server(pipe) => pipe.try_read(buf),
Self::Client(pipe) => pipe.try_read(buf),
}
}
}
pub struct DaemonListener {
name: String,
marker: PathBuf,
security: PipeSecurity,
spare: Option<NamedPipeServer>,
}
impl DaemonListener {
#[allow(clippy::unused_async, clippy::unused_async_trait_impl)]
pub async fn bind(path: &Path) -> io::Result<Self> {
let name = pipe_name(path);
let security = PipeSecurity::owner_only()?;
let spare = security.create_pipe_instance(
ServerOptions::new().first_pipe_instance(true).reject_remote_clients(true),
&name,
)?;
std::fs::write(path, format!("{name}\n{}\n", std::process::id()))?;
Ok(Self { name, marker: path.to_path_buf(), security, spare: Some(spare) })
}
fn new_instance(&self) -> io::Result<NamedPipeServer> {
self.security
.create_pipe_instance(ServerOptions::new().reject_remote_clients(true), &self.name)
}
pub async fn accept(&mut self) -> io::Result<DaemonStream> {
let server = match self.spare.take() {
Some(server) => server,
None => self.new_instance()?,
};
server.connect().await?;
self.spare = self.new_instance().ok();
Ok(DaemonStream::Server(server))
}
pub fn marker_path(&self) -> &Path {
&self.marker
}
}
pub async fn connect(path: &Path) -> io::Result<DaemonStream> {
let name = pipe_name(path);
let deadline = Instant::now() + PIPE_BUSY_RETRY_WINDOW;
loop {
match ClientOptions::new().open(&name) {
Ok(client) => return Ok(DaemonStream::Client(client)),
Err(error)
if error.raw_os_error() == Some(ERROR_PIPE_BUSY.cast_signed())
&& Instant::now() < deadline =>
{
tokio::time::sleep(Duration::from_millis(20)).await;
}
Err(error) => return Err(error),
}
}
}
pub fn peer_is_same_user(_stream: &DaemonStream) -> io::Result<bool> {
Ok(true)
}
#[cfg(test)]
mod tests {
use super::pipe_name;
use std::path::Path;
#[test]
fn pipe_names_are_stable_and_case_insensitive() {
let lower = pipe_name(Path::new(r"c:\users\x\winx\winxd.sock"));
let upper = pipe_name(Path::new(r"C:\Users\X\winx\winxd.sock"));
assert_eq!(lower, upper);
assert!(lower.starts_with(r"\\.\pipe\winx-"));
assert_ne!(lower, pipe_name(Path::new(r"C:\Users\X\winx\other.sock")));
}
}
}
#[cfg(all(test, unix))]
mod unix_claim_tests {
#![allow(clippy::expect_used)]
use super::{connect, lock_path, DaemonListener};
#[tokio::test]
async fn second_bind_fails_while_the_first_listens_and_the_first_stays_reachable() {
let dir = tempfile::tempdir().expect("tempdir");
let socket = dir.path().join("winxd.sock");
let first = DaemonListener::bind(&socket).await.expect("first bind");
let error = DaemonListener::bind(&socket)
.await
.err()
.expect("second bind must not steal the socket");
assert_eq!(error.kind(), std::io::ErrorKind::AddrInUse, "{error}");
assert!(lock_path(&socket).is_file(), "lock file beside the socket");
assert!(connect(&socket).await.is_ok(), "first listener still reachable");
drop(first);
DaemonListener::bind(&socket).await.expect("bind after the first listener is gone");
}
#[tokio::test]
async fn stale_socket_from_a_dead_daemon_is_replaced() {
let dir = tempfile::tempdir().expect("tempdir");
let socket = dir.path().join("winxd.sock");
let stale = tokio::net::UnixListener::bind(&socket).expect("stale listener");
drop(stale);
assert!(socket.exists(), "unix sockets outlive their listener");
let listener = DaemonListener::bind(&socket).await.expect("replace stale socket");
assert!(connect(&socket).await.is_ok());
drop(listener);
}
#[test]
fn lock_path_sits_beside_the_socket() {
assert_eq!(
lock_path(std::path::Path::new("/run/u/winx/winxd.sock")),
std::path::PathBuf::from("/run/u/winx/winxd.sock.lock")
);
}
}