use crate::connection::transport::network_transport::Stream;
use std::os::windows::io::AsRawHandle;
use std::time::Duration;
use tokio::net::windows::named_pipe::NamedPipeClient;
use tracing::{debug, info, warn};
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use winapi::shared::winerror::{ERROR_BROKEN_PIPE, ERROR_PIPE_BUSY, ERROR_PIPE_NOT_CONNECTED};
use winapi::um::namedpipeapi::{PeekNamedPipe, SetNamedPipeHandleState};
use winapi::um::winbase::{PIPE_READMODE_BYTE, PIPE_WAIT};
pub(crate) const NAMED_PIPE_OPEN_TIMEOUT_MS: u32 = 5000;
const LOCAL_LOCALHOST: &str = "localhost";
const LOCAL_IPV4_LOOPBACK: &str = "127.0.0.1";
const LOCAL_IPV6_LOOPBACK: &str = "::1";
pub(crate) fn localize_pipe_path(pipe: &str) -> String {
if !pipe.starts_with("\\\\") {
return pipe.to_string();
}
let after_prefix = &pipe[2..];
let sep = match after_prefix.find('\\') {
Some(pos) => pos,
None => return pipe.to_string(),
};
let server_part = &after_prefix[..sep];
if server_part == "." {
return pipe.to_string();
}
let is_local = server_part.eq_ignore_ascii_case(LOCAL_LOCALHOST)
|| server_part == LOCAL_IPV4_LOOPBACK
|| server_part == LOCAL_IPV6_LOOPBACK
|| hostname::get()
.ok()
.and_then(|n| n.into_string().ok())
.is_some_and(|name| server_part.eq_ignore_ascii_case(&name));
if is_local {
format!("\\\\.{}", &after_prefix[sep..])
} else {
pipe.to_string()
}
}
pub(crate) async fn open_named_pipe_with_retry(
pipe_path: &str,
) -> std::io::Result<NamedPipeClient> {
use std::time::Instant;
use tokio::net::windows::named_pipe::ClientOptions;
info!(pipe_path, "Opening named pipe connection");
let start_time = Instant::now();
let timeout_duration = Duration::from_millis(NAMED_PIPE_OPEN_TIMEOUT_MS as u64);
loop {
match ClientOptions::new()
.pipe_mode(tokio::net::windows::named_pipe::PipeMode::Message)
.open(pipe_path)
{
Ok(client) => {
debug!(pipe_path, elapsed_ms = ?start_time.elapsed().as_millis(), "Named pipe connection established");
return Ok(client);
}
Err(e) => {
if e.raw_os_error() == Some(ERROR_PIPE_BUSY as i32) {
let elapsed = start_time.elapsed();
warn!(pipe_path, elapsed_ms = ?elapsed.as_millis(), "Named pipe busy, waiting for available instance");
if elapsed >= timeout_duration {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!(
"Named pipe connection timed out after {}ms: all pipe instances busy",
elapsed.as_millis()
),
));
}
let remaining_ms = timeout_duration
.checked_sub(elapsed)
.unwrap_or(Duration::from_millis(0))
.as_millis() as u32;
if remaining_ms == 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"Named pipe connection timed out: all pipe instances busy",
));
}
let pipe_path_owned = pipe_path.to_string();
match tokio::task::spawn_blocking(move || {
wait_for_named_pipe(&pipe_path_owned, remaining_ms)
})
.await
{
Ok(Ok(())) => {
debug!("Named pipe became available, retrying connection");
continue;
}
Ok(Err(wait_err)) => {
return Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!(
"Named pipe wait failed after {}ms: {}",
elapsed.as_millis(),
wait_err
),
));
}
Err(join_err) => {
return Err(std::io::Error::other(format!(
"Failed to wait for named pipe: {join_err}"
)));
}
}
} else {
return Err(e);
}
}
}
}
}
fn wait_for_named_pipe(pipe_path: &str, timeout_ms: u32) -> std::io::Result<()> {
use winapi::um::namedpipeapi::WaitNamedPipeW;
debug!(pipe_path, timeout_ms, "Calling WaitNamedPipeW");
let wide_path: Vec<u16> = OsStr::new(pipe_path)
.encode_wide()
.chain(std::iter::once(0)) .collect();
let result = unsafe { WaitNamedPipeW(wide_path.as_ptr(), timeout_ms) };
if result == 0 {
return Err(std::io::Error::last_os_error());
}
Ok(())
}
impl Stream for NamedPipeClient {
fn tls_handshake_starting(&mut self) {
debug!("TLS handshake starting on Named Pipe (Message mode)");
}
fn tls_handshake_completed(&mut self) {
debug!("TLS handshake completed, switching Named Pipe to Byte mode");
let handle = self.as_raw_handle();
let mut mode: u32 = PIPE_READMODE_BYTE | PIPE_WAIT;
let result = unsafe {
SetNamedPipeHandleState(
handle as *mut _,
&mut mode as *mut u32 as *mut _,
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
if result == 0 {
let error = std::io::Error::last_os_error();
warn!("Failed to switch Named Pipe to Byte mode: {}", error);
} else {
info!("Named Pipe switched to Byte mode for streaming reads");
}
}
fn is_connection_dead(&self) -> bool {
let handle = self.as_raw_handle();
let ok = unsafe {
PeekNamedPipe(
handle as *mut _,
std::ptr::null_mut(),
0,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
)
};
if ok != 0 {
return false;
}
matches!(
std::io::Error::last_os_error().raw_os_error(),
Some(code) if code == ERROR_BROKEN_PIPE as i32 || code == ERROR_PIPE_NOT_CONNECTED as i32
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn localize_already_local_dot() {
let path = r"\\.\pipe\sql\query";
assert_eq!(localize_pipe_path(path), path);
}
#[test]
fn localize_localhost() {
assert_eq!(
localize_pipe_path(r"\\localhost\pipe\sql\query"),
r"\\.\pipe\sql\query"
);
}
#[test]
fn localize_localhost_case_insensitive() {
assert_eq!(
localize_pipe_path(r"\\LOCALHOST\pipe\sql\query"),
r"\\.\pipe\sql\query"
);
}
#[test]
fn localize_ipv4_loopback() {
assert_eq!(
localize_pipe_path(r"\\127.0.0.1\pipe\sql\query"),
r"\\.\pipe\sql\query"
);
}
#[test]
fn localize_ipv6_loopback() {
assert_eq!(
localize_pipe_path(r"\\::1\pipe\sql\query"),
r"\\.\pipe\sql\query"
);
}
#[test]
fn localize_hostname_match() {
let hostname = hostname::get()
.ok()
.and_then(|h| h.into_string().ok())
.expect("hostname required");
let input = format!(r"\\{}\pipe\sql\query", hostname);
assert_eq!(localize_pipe_path(&input), r"\\.\pipe\sql\query");
}
#[test]
fn localize_remote_server_unchanged() {
let path = r"\\remoteserver\pipe\sql\query";
assert_eq!(localize_pipe_path(path), path);
}
#[test]
fn localize_no_prefix_unchanged() {
assert_eq!(localize_pipe_path("sql\\query"), "sql\\query");
}
#[test]
fn localize_single_backslash_unchanged() {
assert_eq!(localize_pipe_path(r"\pipe"), r"\pipe");
}
#[test]
fn localize_no_separator_after_prefix() {
assert_eq!(localize_pipe_path(r"\\server"), r"\\server");
}
}