use std::ffi::OsString;
use std::time::Duration;
use tokio::net::windows::named_pipe::{NamedPipeClient, NamedPipeServer};
use weida_core::{Error, LossCause, WindowsPrincipal};
use crate::Exec;
const BUSY_BUDGET: Duration = Duration::from_secs(5);
const BUSY_STEP: Duration = Duration::from_millis(10);
#[derive(Debug)]
pub struct BoundPipe {
path: OsString,
dacl: weida_winpipe::OwnerOnlyDacl,
}
impl BoundPipe {
pub fn bind(path: impl Into<OsString>) -> Result<(BoundPipe, NamedPipeServer), Error> {
let path = path.into();
let dacl = weida_winpipe::OwnerOnlyDacl::for_current_user().map_err(Error::Io)?;
let first = weida_winpipe::create_instance(&path, &dacl, true).map_err(|e| {
if e.kind() == std::io::ErrorKind::PermissionDenied {
Error::InvalidAddress(format!(
"{} already exists: a pipe name that is taken is not ours to serve",
path.to_string_lossy()
))
} else {
Error::Io(e)
}
})?;
Ok((BoundPipe { path, dacl }, first))
}
pub fn next_instance(&self) -> Result<NamedPipeServer, Error> {
weida_winpipe::create_instance(&self.path, &self.dacl, false).map_err(Error::Io)
}
pub fn path(&self) -> &std::ffi::OsStr {
&self.path
}
}
pub async fn connect_pipe(exec: &Exec, path: &std::ffi::OsStr) -> Result<NamedPipeClient, Error> {
let started = std::time::Instant::now();
loop {
let attempt = {
let _guard = exec.enter();
weida_winpipe::open_client(path)
};
match attempt {
Ok(client) => return Ok(client),
Err(e) if weida_winpipe::is_pipe_busy(&e) => {
if started.elapsed() >= BUSY_BUDGET {
return Err(Error::Transport(format!(
"{}: every pipe instance stayed busy for {BUSY_BUDGET:?}",
path.to_string_lossy()
)));
}
exec.sleep(BUSY_STEP).await;
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Err(Error::ConnectionLost(LossCause::PeerClosed));
}
Err(e) => return Err(Error::Io(e)),
}
}
}
pub fn client_principal(server: &NamedPipeServer) -> Result<WindowsPrincipal, Error> {
weida_winpipe::client_peer(server)
.map(into_principal)
.map_err(Error::Io)
}
pub fn server_principal(client: &NamedPipeClient) -> Result<WindowsPrincipal, Error> {
weida_winpipe::server_peer(client)
.map(into_principal)
.map_err(Error::Io)
}
fn into_principal(peer: weida_winpipe::PipePeer) -> WindowsPrincipal {
WindowsPrincipal {
sid: peer.sid.into(),
pid: Some(peer.pid),
}
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
fn name(tag: &str) -> String {
format!(r"\\.\pipe\weida-runtime-{}-{tag}", std::process::id())
}
#[tokio::test]
async fn both_ends_name_this_process() {
let exec = Exec::current().expect("runtime");
let path = name("who");
let (bound, server) = BoundPipe::bind(path.clone()).expect("bind");
let client = connect_pipe(&exec, bound.path()).await.expect("connect");
server.connect().await.expect("accept");
let mut client = client;
let server_seen_by_client = server_principal(&client).expect("owner");
client.write_all(b"x").await.expect("write");
let mut byte = [0u8; 1];
let mut server = server;
server.read_exact(&mut byte).await.expect("read");
let client_seen_by_server = client_principal(&server).expect("token");
assert_eq!(client_seen_by_server.sid, server_seen_by_client.sid);
assert_eq!(client_seen_by_server.pid, Some(std::process::id()));
assert_eq!(server_seen_by_client.pid, Some(std::process::id()));
}
#[tokio::test]
async fn a_taken_name_is_not_bound_twice() {
let path = name("taken");
let (_bound, _first) = BoundPipe::bind(path.clone()).expect("bind");
let err = BoundPipe::bind(path).unwrap_err();
assert!(matches!(err, Error::InvalidAddress(_)), "{err:?}");
}
#[tokio::test]
async fn an_absent_pipe_is_a_closed_peer() {
let exec = Exec::current().expect("runtime");
let path = name("absent");
let err = connect_pipe(&exec, std::ffi::OsStr::new(&path))
.await
.unwrap_err();
assert!(
matches!(err, Error::ConnectionLost(LossCause::PeerClosed)),
"{err:?}"
);
}
}