winx_code_agent/daemon/
transport.rs1use std::io;
10use std::path::Path;
11
12#[cfg(unix)]
13pub use unix_impl::{connect, peer_is_same_user, DaemonListener, DaemonStream};
14#[cfg(windows)]
15pub use windows_impl::{connect, peer_is_same_user, pipe_name, DaemonListener, DaemonStream};
16
17#[cfg_attr(not(unix), allow(clippy::unused_async))] pub async fn restrict_to_owner(path: &Path, mode: u32) -> io::Result<()> {
21 #[cfg(unix)]
22 {
23 use std::os::unix::fs::PermissionsExt as _;
24 tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(mode)).await
25 }
26 #[cfg(not(unix))]
27 {
28 let _ = (path, mode);
29 Ok(())
30 }
31}
32
33#[cfg(unix)]
34mod unix_impl {
35 use std::io;
36 use std::path::Path;
37
38 use tokio::net::{UnixListener, UnixStream};
39
40 pub type DaemonStream = UnixStream;
41
42 pub struct DaemonListener {
43 inner: UnixListener,
44 }
45
46 impl DaemonListener {
47 pub fn bind(path: &Path) -> io::Result<Self> {
48 Ok(Self { inner: UnixListener::bind(path)? })
49 }
50
51 pub async fn accept(&mut self) -> io::Result<DaemonStream> {
52 self.inner.accept().await.map(|(stream, _)| stream)
53 }
54 }
55
56 pub async fn connect(path: &Path) -> io::Result<DaemonStream> {
57 UnixStream::connect(path).await
58 }
59
60 pub fn peer_is_same_user(stream: &DaemonStream) -> io::Result<bool> {
62 Ok(stream.peer_cred()?.uid() == crate::os::unix::effective_uid())
63 }
64}
65
66#[cfg(windows)]
67mod windows_impl {
68 use std::io;
69 use std::path::{Path, PathBuf};
70 use std::pin::Pin;
71 use std::task::{Context, Poll};
72 use std::time::{Duration, Instant};
73
74 use sha2::{Digest as _, Sha256};
75 use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
76 use tokio::net::windows::named_pipe::{
77 ClientOptions, NamedPipeClient, NamedPipeServer, ServerOptions,
78 };
79 use windows_sys::Win32::Foundation::ERROR_PIPE_BUSY;
80
81 const PIPE_BUSY_RETRY_WINDOW: Duration = Duration::from_secs(2);
85
86 pub fn pipe_name(path: &Path) -> String {
90 let normalized = path.to_string_lossy().to_lowercase();
91 let digest = Sha256::digest(normalized.as_bytes());
92 let mut tag = String::with_capacity(24);
93 for byte in &digest[..12] {
94 use std::fmt::Write as _;
95 let _ = write!(tag, "{byte:02x}");
96 }
97 format!(r"\\.\pipe\winx-{tag}")
98 }
99
100 #[derive(Debug)]
102 pub enum DaemonStream {
103 Server(NamedPipeServer),
104 Client(NamedPipeClient),
105 }
106
107 impl AsyncRead for DaemonStream {
108 fn poll_read(
109 self: Pin<&mut Self>,
110 cx: &mut Context<'_>,
111 buf: &mut ReadBuf<'_>,
112 ) -> Poll<io::Result<()>> {
113 match self.get_mut() {
114 Self::Server(pipe) => Pin::new(pipe).poll_read(cx, buf),
115 Self::Client(pipe) => Pin::new(pipe).poll_read(cx, buf),
116 }
117 }
118 }
119
120 impl AsyncWrite for DaemonStream {
121 fn poll_write(
122 self: Pin<&mut Self>,
123 cx: &mut Context<'_>,
124 data: &[u8],
125 ) -> Poll<io::Result<usize>> {
126 match self.get_mut() {
127 Self::Server(pipe) => Pin::new(pipe).poll_write(cx, data),
128 Self::Client(pipe) => Pin::new(pipe).poll_write(cx, data),
129 }
130 }
131
132 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
133 match self.get_mut() {
134 Self::Server(pipe) => Pin::new(pipe).poll_flush(cx),
135 Self::Client(pipe) => Pin::new(pipe).poll_flush(cx),
136 }
137 }
138
139 fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
140 match self.get_mut() {
141 Self::Server(pipe) => Pin::new(pipe).poll_shutdown(cx),
142 Self::Client(pipe) => Pin::new(pipe).poll_shutdown(cx),
143 }
144 }
145 }
146
147 impl DaemonStream {
148 pub fn try_read(&self, buf: &mut [u8]) -> io::Result<usize> {
151 match self {
152 Self::Server(pipe) => pipe.try_read(buf),
153 Self::Client(pipe) => pipe.try_read(buf),
154 }
155 }
156 }
157
158 pub struct DaemonListener {
160 name: String,
161 marker: PathBuf,
162 spare: Option<NamedPipeServer>,
163 }
164
165 impl DaemonListener {
166 pub fn bind(path: &Path) -> io::Result<Self> {
167 let name = pipe_name(path);
168 let spare = ServerOptions::new()
174 .first_pipe_instance(true)
175 .reject_remote_clients(true)
176 .create(&name)?;
177 std::fs::write(path, format!("{name}\n{}\n", std::process::id()))?;
178 Ok(Self { name, marker: path.to_path_buf(), spare: Some(spare) })
179 }
180
181 pub async fn accept(&mut self) -> io::Result<DaemonStream> {
182 let server = match self.spare.take() {
183 Some(server) => server,
184 None => ServerOptions::new().reject_remote_clients(true).create(&self.name)?,
185 };
186 server.connect().await?;
187 self.spare = ServerOptions::new().reject_remote_clients(true).create(&self.name).ok();
190 Ok(DaemonStream::Server(server))
191 }
192
193 pub fn marker_path(&self) -> &Path {
194 &self.marker
195 }
196 }
197
198 pub async fn connect(path: &Path) -> io::Result<DaemonStream> {
201 let name = pipe_name(path);
202 let deadline = Instant::now() + PIPE_BUSY_RETRY_WINDOW;
203 loop {
204 match ClientOptions::new().open(&name) {
205 Ok(client) => return Ok(DaemonStream::Client(client)),
206 Err(error)
207 if error.raw_os_error() == Some(ERROR_PIPE_BUSY.cast_signed())
208 && Instant::now() < deadline =>
209 {
210 tokio::time::sleep(Duration::from_millis(20)).await;
211 }
212 Err(error) => return Err(error),
213 }
214 }
215 }
216
217 pub fn peer_is_same_user(_stream: &DaemonStream) -> io::Result<bool> {
220 Ok(true)
221 }
222
223 #[cfg(test)]
224 mod tests {
225 use super::pipe_name;
226 use std::path::Path;
227
228 #[test]
229 fn pipe_names_are_stable_and_case_insensitive() {
230 let lower = pipe_name(Path::new(r"c:\users\x\winx\winxd.sock"));
231 let upper = pipe_name(Path::new(r"C:\Users\X\winx\winxd.sock"));
232 assert_eq!(lower, upper);
233 assert!(lower.starts_with(r"\\.\pipe\winx-"));
234 assert_ne!(lower, pipe_name(Path::new(r"C:\Users\X\winx\other.sock")));
235 }
236 }
237}