chore: import upstream snapshot with attribution
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
FreeBSD Smoke / FreeBSD Smoke (x86_64) (push) Has been cancelled
CI / Quality Guardrails (push) Has been cancelled
CI / Build & Test (macos-latest) (push) Has been cancelled
CI / Build & Test (ubuntu-latest) (push) Has been cancelled
CI / Build & Test (windows-latest) (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / PowerShell Syntax (push) Has been cancelled
CI / Windows Cross-Target Check (Linux) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
#[cfg(unix)]
|
||||
mod unix;
|
||||
#[cfg(unix)]
|
||||
pub use unix::*;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
#[cfg(windows)]
|
||||
pub use windows::*;
|
||||
@@ -0,0 +1,19 @@
|
||||
pub use tokio::net::UnixListener as Listener;
|
||||
pub use tokio::net::UnixStream as Stream;
|
||||
pub use tokio::net::unix::OwnedReadHalf as ReadHalf;
|
||||
pub use tokio::net::unix::OwnedWriteHalf as WriteHalf;
|
||||
|
||||
pub use std::os::unix::net::UnixStream as SyncStream;
|
||||
|
||||
pub fn is_socket_path(path: &std::path::Path) -> bool {
|
||||
path.exists()
|
||||
}
|
||||
|
||||
pub fn remove_socket(path: &std::path::Path) {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
/// Create a connected pair of UnixStreams (for in-process bridging).
|
||||
pub fn stream_pair() -> std::io::Result<(Stream, Stream)> {
|
||||
Stream::pair()
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use tokio::net::windows::named_pipe::{
|
||||
ClientOptions, NamedPipeClient, NamedPipeServer, ServerOptions,
|
||||
};
|
||||
|
||||
/// Convert a filesystem path to a Windows named pipe path.
|
||||
///
|
||||
/// e.g. `/run/user/1000/jcode.sock` -> `\\.\pipe\jcode`
|
||||
/// e.g. `/run/user/1000/jcode/myserver.sock` -> `\\.\pipe\jcode-myserver`
|
||||
fn path_to_pipe_name(path: &Path) -> String {
|
||||
let stem: String = path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("jcode")
|
||||
.chars()
|
||||
.filter(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_'))
|
||||
.take(32)
|
||||
.collect();
|
||||
let stem = if stem.is_empty() { "jcode" } else { &stem };
|
||||
let normalized = path
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/")
|
||||
.to_ascii_lowercase();
|
||||
let digest = Sha256::digest(normalized.as_bytes());
|
||||
let hash = hex::encode(digest);
|
||||
format!(r"\\.\pipe\{}-{}", stem, &hash[..16])
|
||||
}
|
||||
|
||||
/// Listener wraps a Windows named pipe server, providing an accept loop
|
||||
/// that matches the UnixListener interface.
|
||||
pub struct Listener {
|
||||
pipe_name: String,
|
||||
current_server: NamedPipeServer,
|
||||
}
|
||||
|
||||
impl Listener {
|
||||
pub fn bind(path: &Path) -> io::Result<Self> {
|
||||
let pipe_name = path_to_pipe_name(path);
|
||||
match ServerOptions::new()
|
||||
.first_pipe_instance(true)
|
||||
.create(&pipe_name)
|
||||
{
|
||||
Ok(server) => Ok(Self {
|
||||
pipe_name,
|
||||
current_server: server,
|
||||
}),
|
||||
Err(e)
|
||||
if e.raw_os_error()
|
||||
== Some(windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED as i32) =>
|
||||
{
|
||||
eprintln!(
|
||||
"[windows] Named pipe {} busy (access denied), retrying without first_pipe_instance",
|
||||
pipe_name
|
||||
);
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
let server = ServerOptions::new().create(&pipe_name)?;
|
||||
Ok(Self {
|
||||
pipe_name,
|
||||
current_server: server,
|
||||
})
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn accept(&mut self) -> io::Result<(Stream, PipeAddr)> {
|
||||
self.current_server.connect().await?;
|
||||
|
||||
let connected = std::mem::replace(
|
||||
&mut self.current_server,
|
||||
ServerOptions::new().create(&self.pipe_name)?,
|
||||
);
|
||||
|
||||
Ok((Stream::Server(connected), PipeAddr))
|
||||
}
|
||||
}
|
||||
|
||||
/// Placeholder for the "address" of a named pipe connection.
|
||||
pub struct PipeAddr;
|
||||
|
||||
/// Stream wraps either a NamedPipeServer (accepted connection) or
|
||||
/// NamedPipeClient (outgoing connection).
|
||||
pub enum Stream {
|
||||
Server(NamedPipeServer),
|
||||
Client(NamedPipeClient),
|
||||
}
|
||||
|
||||
impl Stream {
|
||||
pub async fn connect(path: impl AsRef<Path>) -> io::Result<Self> {
|
||||
let pipe_name = path_to_pipe_name(path.as_ref());
|
||||
loop {
|
||||
match ClientOptions::new().open(&pipe_name) {
|
||||
Ok(client) => return Ok(Stream::Client(client)),
|
||||
Err(e)
|
||||
if e.raw_os_error()
|
||||
== Some(windows_sys::Win32::Foundation::ERROR_PIPE_BUSY as i32) =>
|
||||
{
|
||||
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_split(self) -> (ReadHalf, WriteHalf) {
|
||||
let (read, write) = tokio::io::split(self);
|
||||
(ReadHalf { inner: read }, WriteHalf { inner: write })
|
||||
}
|
||||
|
||||
pub fn split(&mut self) -> (SplitReadRef<'_>, SplitWriteRef<'_>) {
|
||||
let (read, write) = tokio::io::split(self);
|
||||
(SplitReadRef { inner: read }, SplitWriteRef { inner: write })
|
||||
}
|
||||
|
||||
pub fn pair() -> io::Result<(Self, Self)> {
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
static PAIR_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
let counter = PAIR_COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let pipe_name = format!(r"\\.\pipe\jcode-pair-{}-{}", std::process::id(), counter);
|
||||
let server = ServerOptions::new()
|
||||
.first_pipe_instance(true)
|
||||
.create(&pipe_name)?;
|
||||
let client = ClientOptions::new().open(&pipe_name)?;
|
||||
|
||||
// The client connected when we opened it above, but the server must
|
||||
// call connect() to transition into the connected state. For an
|
||||
// already-connected client this returns immediately.
|
||||
//
|
||||
// We use a short-lived runtime-free poll: since the client already
|
||||
// connected synchronously, the server's connect future will resolve
|
||||
// on the first poll.
|
||||
use std::future::Future;
|
||||
use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
|
||||
fn dummy_raw_waker() -> RawWaker {
|
||||
fn no_op(_: *const ()) {}
|
||||
fn clone(p: *const ()) -> RawWaker {
|
||||
RawWaker::new(p, &VTABLE)
|
||||
}
|
||||
const VTABLE: RawWakerVTable = RawWakerVTable::new(clone, no_op, no_op, no_op);
|
||||
RawWaker::new(std::ptr::null(), &VTABLE)
|
||||
}
|
||||
let waker = unsafe { Waker::from_raw(dummy_raw_waker()) };
|
||||
let mut cx = Context::from_waker(&waker);
|
||||
let mut fut = server.connect();
|
||||
let pinned = unsafe { std::pin::Pin::new_unchecked(&mut fut) };
|
||||
match pinned.poll(&mut cx) {
|
||||
Poll::Ready(Ok(())) => {}
|
||||
Poll::Ready(Err(e)) => return Err(e),
|
||||
Poll::Pending => {
|
||||
// Should not happen since the client already connected.
|
||||
// Drop the future and proceed - the pipe is still usable.
|
||||
}
|
||||
}
|
||||
drop(fut);
|
||||
|
||||
Ok((Stream::Server(server), Stream::Client(client)))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for Stream {
|
||||
fn poll_read(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
match self.get_mut() {
|
||||
Stream::Server(s) => std::pin::Pin::new(s).poll_read(cx, buf),
|
||||
Stream::Client(c) => std::pin::Pin::new(c).poll_read(cx, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for Stream {
|
||||
fn poll_write(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<io::Result<usize>> {
|
||||
match self.get_mut() {
|
||||
Stream::Server(s) => std::pin::Pin::new(s).poll_write(cx, buf),
|
||||
Stream::Client(c) => std::pin::Pin::new(c).poll_write(cx, buf),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
match self.get_mut() {
|
||||
Stream::Server(s) => std::pin::Pin::new(s).poll_flush(cx),
|
||||
Stream::Client(c) => std::pin::Pin::new(c).poll_flush(cx),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
match self.get_mut() {
|
||||
Stream::Server(s) => std::pin::Pin::new(s).poll_shutdown(cx),
|
||||
Stream::Client(c) => std::pin::Pin::new(c).poll_shutdown(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Owned read half of a Stream, created by `into_split()`.
|
||||
pub struct ReadHalf {
|
||||
inner: tokio::io::ReadHalf<Stream>,
|
||||
}
|
||||
|
||||
impl AsyncRead for ReadHalf {
|
||||
fn poll_read(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
std::pin::Pin::new(&mut self.get_mut().inner).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Owned write half of a Stream, created by `into_split()`.
|
||||
pub struct WriteHalf {
|
||||
inner: tokio::io::WriteHalf<Stream>,
|
||||
}
|
||||
|
||||
impl AsyncWrite for WriteHalf {
|
||||
fn poll_write(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<io::Result<usize>> {
|
||||
std::pin::Pin::new(&mut self.get_mut().inner).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
std::pin::Pin::new(&mut self.get_mut().inner).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
std::pin::Pin::new(&mut self.get_mut().inner).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrowed read reference for `stream.split()`.
|
||||
pub struct SplitReadRef<'a> {
|
||||
inner: tokio::io::ReadHalf<&'a mut Stream>,
|
||||
}
|
||||
|
||||
impl<'a> AsyncRead for SplitReadRef<'a> {
|
||||
fn poll_read(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &mut ReadBuf<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
std::pin::Pin::new(&mut self.get_mut().inner).poll_read(cx, buf)
|
||||
}
|
||||
}
|
||||
|
||||
/// Borrowed write reference for `stream.split()`.
|
||||
pub struct SplitWriteRef<'a> {
|
||||
inner: tokio::io::WriteHalf<&'a mut Stream>,
|
||||
}
|
||||
|
||||
impl<'a> AsyncWrite for SplitWriteRef<'a> {
|
||||
fn poll_write(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
buf: &[u8],
|
||||
) -> std::task::Poll<io::Result<usize>> {
|
||||
std::pin::Pin::new(&mut self.get_mut().inner).poll_write(cx, buf)
|
||||
}
|
||||
|
||||
fn poll_flush(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
std::pin::Pin::new(&mut self.get_mut().inner).poll_flush(cx)
|
||||
}
|
||||
|
||||
fn poll_shutdown(
|
||||
self: std::pin::Pin<&mut Self>,
|
||||
cx: &mut std::task::Context<'_>,
|
||||
) -> std::task::Poll<io::Result<()>> {
|
||||
std::pin::Pin::new(&mut self.get_mut().inner).poll_shutdown(cx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronous named pipe stream for blocking IPC (used by communicate tool).
|
||||
pub struct SyncStream {
|
||||
handle: std::fs::File,
|
||||
}
|
||||
|
||||
impl SyncStream {
|
||||
pub fn connect(path: &Path) -> io::Result<Self> {
|
||||
use std::fs::OpenOptions;
|
||||
let pipe_name = path_to_pipe_name(path);
|
||||
let file = OpenOptions::new().read(true).write(true).open(&pipe_name)?;
|
||||
Ok(Self { handle: file })
|
||||
}
|
||||
|
||||
pub fn set_read_timeout(&self, timeout: Option<std::time::Duration>) -> io::Result<()> {
|
||||
let _ = timeout;
|
||||
// std::fs::File-backed named pipes do not expose socket-style read timeouts.
|
||||
// The communicate tool only uses this to avoid hanging forever; on Windows
|
||||
// we currently rely on the server side to respond promptly.
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl io::Read for SyncStream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.handle.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl io::Write for SyncStream {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.handle.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.handle.flush()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_socket_path(path: &Path) -> bool {
|
||||
let pipe_name = path_to_pipe_name(path);
|
||||
ClientOptions::new().open(&pipe_name).is_ok()
|
||||
}
|
||||
|
||||
pub fn remove_socket(path: &Path) {
|
||||
let pipe_name = path_to_pipe_name(path);
|
||||
if ClientOptions::new().open(&pipe_name).is_ok() {
|
||||
eprintln!(
|
||||
"[windows] Named pipe {} still open, will be replaced by new server",
|
||||
pipe_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stream_pair() -> io::Result<(Stream, Stream)> {
|
||||
Stream::pair()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
#[test]
|
||||
fn pipe_name_is_stable_and_normalizes_case_and_separators() {
|
||||
let a = path_to_pipe_name(Path::new(r"C:\Temp\Jcode\server.sock"));
|
||||
let b = path_to_pipe_name(Path::new("c:/temp/jcode/server.sock"));
|
||||
assert_eq!(a, b, "pipe names should be normalized consistently");
|
||||
assert!(
|
||||
a.starts_with(r"\\.\pipe\server-"),
|
||||
"unexpected pipe name: {}",
|
||||
a
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipe_name_falls_back_when_stem_is_empty() {
|
||||
let name = path_to_pipe_name(Path::new("..."));
|
||||
assert!(
|
||||
name.starts_with(r"\\.\pipe\jcode-"),
|
||||
"unexpected pipe name: {}",
|
||||
name
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stream_pair_round_trips_bytes() {
|
||||
let (mut a, mut b) = stream_pair().expect("create stream pair");
|
||||
a.write_all(b"ping").await.expect("write to pipe");
|
||||
a.flush().await.expect("flush pipe");
|
||||
|
||||
let mut buf = [0u8; 4];
|
||||
b.read_exact(&mut buf).await.expect("read from pipe");
|
||||
assert_eq!(&buf, b"ping");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn split_stream_supports_concurrent_read_and_write() {
|
||||
let (a, mut b) = stream_pair().expect("create stream pair");
|
||||
let (mut read, mut write) = a.into_split();
|
||||
|
||||
let peer = tokio::spawn(async move {
|
||||
let mut request = [0u8; 4];
|
||||
b.read_exact(&mut request).await.expect("read request");
|
||||
assert_eq!(&request, b"ping");
|
||||
b.write_all(b"pong").await.expect("write response");
|
||||
b.flush().await.expect("flush response");
|
||||
});
|
||||
|
||||
let exchange = async {
|
||||
write.write_all(b"ping").await.expect("write request");
|
||||
write.flush().await.expect("flush request");
|
||||
let mut response = [0u8; 4];
|
||||
read.read_exact(&mut response).await.expect("read response");
|
||||
assert_eq!(&response, b"pong");
|
||||
};
|
||||
|
||||
tokio::time::timeout(std::time::Duration::from_secs(5), exchange)
|
||||
.await
|
||||
.expect("split stream exchange should not stall");
|
||||
peer.await.expect("peer task");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user