1use std::io;
10use std::path::{Path, PathBuf};
11#[cfg(unix)]
12use std::time::{Duration, Instant};
13
14#[cfg(unix)]
15pub use unix_impl::{connect, peer_is_same_user, DaemonListener, DaemonStream};
16#[cfg(windows)]
17pub use windows_impl::{connect, peer_is_same_user, pipe_name, DaemonListener, DaemonStream};
18
19#[cfg_attr(not(unix), allow(clippy::unused_async))] pub async fn restrict_to_owner(path: &Path, mode: u32) -> io::Result<()> {
23 #[cfg(unix)]
24 {
25 use std::os::unix::fs::PermissionsExt as _;
26 tokio::fs::set_permissions(path, std::fs::Permissions::from_mode(mode)).await
27 }
28 #[cfg(not(unix))]
29 {
30 let _ = (path, mode);
31 Ok(())
32 }
33}
34
35pub fn lock_path(socket: &Path) -> PathBuf {
37 let mut name = socket.file_name().map(std::ffi::OsStr::to_os_string).unwrap_or_default();
38 name.push(".lock");
39 socket.with_file_name(name)
40}
41
42#[cfg(unix)]
48const CLAIM_RETRY_WINDOW: Duration = Duration::from_millis(250);
49
50#[cfg(unix)]
53async fn claim_lock(path: &Path) -> io::Result<std::fs::File> {
54 use std::os::unix::fs::OpenOptionsExt as _;
55 let deadline = Instant::now() + CLAIM_RETRY_WINDOW;
56 loop {
57 let file = std::fs::OpenOptions::new()
58 .read(true)
59 .write(true)
60 .create(true)
61 .truncate(false)
62 .mode(0o600)
63 .open(path)?;
64 if crate::os::unix::try_lock_exclusive(&file)? {
65 return Ok(file);
66 }
67 if Instant::now() >= deadline {
68 return Err(io::Error::new(
69 io::ErrorKind::AddrInUse,
70 format!("another daemon is starting or listening (lock {})", path.display()),
71 ));
72 }
73 drop(file);
74 tokio::time::sleep(Duration::from_millis(10)).await;
75 }
76}
77
78#[cfg(unix)]
79mod unix_impl {
80 use std::io;
81 use std::path::Path;
82
83 use tokio::net::{UnixListener, UnixStream};
84
85 pub type DaemonStream = UnixStream;
86
87 pub struct DaemonListener {
88 inner: UnixListener,
89 _lock: std::fs::File,
91 }
92
93 impl DaemonListener {
94 pub async fn bind(path: &Path) -> io::Result<Self> {
104 let lock = super::claim_lock(&super::lock_path(path)).await?;
105 if tokio::fs::try_exists(path).await? {
106 if connect(path).await.is_ok() {
107 return Err(io::Error::new(
108 io::ErrorKind::AddrInUse,
109 format!("a daemon is already listening at {}", path.display()),
110 ));
111 }
112 tokio::fs::remove_file(path).await?;
113 }
114 Ok(Self { inner: UnixListener::bind(path)?, _lock: lock })
115 }
116
117 pub async fn accept(&mut self) -> io::Result<DaemonStream> {
118 self.inner.accept().await.map(|(stream, _)| stream)
119 }
120 }
121
122 pub async fn connect(path: &Path) -> io::Result<DaemonStream> {
123 UnixStream::connect(path).await
124 }
125
126 pub fn peer_is_same_user(stream: &DaemonStream) -> io::Result<bool> {
128 Ok(stream.peer_cred()?.uid() == crate::os::unix::effective_uid())
129 }
130}
131
132#[cfg(windows)]
133mod windows_impl {
134 use std::io;
135 use std::path::{Path, PathBuf};
136 use std::pin::Pin;
137 use std::task::{Context, Poll};
138 use std::time::{Duration, Instant};
139
140 use sha2::{Digest as _, Sha256};
141 use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
142 use tokio::net::windows::named_pipe::{
143 ClientOptions, NamedPipeClient, NamedPipeServer, ServerOptions,
144 };
145 use windows_sys::Win32::Foundation::ERROR_PIPE_BUSY;
146
147 use crate::os::windows::PipeSecurity;
148
149 const PIPE_BUSY_RETRY_WINDOW: Duration = Duration::from_secs(2);
153
154 pub fn pipe_name(path: &Path) -> String {
158 let normalized = path.to_string_lossy().to_lowercase();
159 let digest = Sha256::digest(normalized.as_bytes());
160 let mut tag = String::with_capacity(24);
161 for byte in &digest[..12] {
162 use std::fmt::Write as _;
163 let _ = write!(tag, "{byte:02x}");
164 }
165 format!(r"\\.\pipe\winx-{tag}")
166 }
167
168 #[derive(Debug)]
170 pub enum DaemonStream {
171 Server(NamedPipeServer),
172 Client(NamedPipeClient),
173 }
174
175 impl AsyncRead for DaemonStream {
176 fn poll_read(
177 self: Pin<&mut Self>,
178 cx: &mut Context<'_>,
179 buf: &mut ReadBuf<'_>,
180 ) -> Poll<io::Result<()>> {
181 match self.get_mut() {
182 Self::Server(pipe) => Pin::new(pipe).poll_read(cx, buf),
183 Self::Client(pipe) => Pin::new(pipe).poll_read(cx, buf),
184 }
185 }
186 }
187
188 impl AsyncWrite for DaemonStream {
189 fn poll_write(
190 self: Pin<&mut Self>,
191 cx: &mut Context<'_>,
192 data: &[u8],
193 ) -> Poll<io::Result<usize>> {
194 match self.get_mut() {
195 Self::Server(pipe) => Pin::new(pipe).poll_write(cx, data),
196 Self::Client(pipe) => Pin::new(pipe).poll_write(cx, data),
197 }
198 }
199
200 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
201 match self.get_mut() {
202 Self::Server(pipe) => Pin::new(pipe).poll_flush(cx),
203 Self::Client(pipe) => Pin::new(pipe).poll_flush(cx),
204 }
205 }
206
207 fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
208 match self.get_mut() {
209 Self::Server(pipe) => Pin::new(pipe).poll_shutdown(cx),
210 Self::Client(pipe) => Pin::new(pipe).poll_shutdown(cx),
211 }
212 }
213 }
214
215 impl DaemonStream {
216 pub fn try_read(&self, buf: &mut [u8]) -> io::Result<usize> {
219 match self {
220 Self::Server(pipe) => pipe.try_read(buf),
221 Self::Client(pipe) => pipe.try_read(buf),
222 }
223 }
224 }
225
226 pub struct DaemonListener {
228 name: String,
229 marker: PathBuf,
230 security: PipeSecurity,
231 spare: Option<NamedPipeServer>,
232 }
233
234 impl DaemonListener {
235 #[allow(clippy::unused_async, clippy::unused_async_trait_impl)]
240 pub async fn bind(path: &Path) -> io::Result<Self> {
241 let name = pipe_name(path);
242 let security = PipeSecurity::owner_only()?;
249 let spare = security.create_pipe_instance(
250 ServerOptions::new().first_pipe_instance(true).reject_remote_clients(true),
251 &name,
252 )?;
253 std::fs::write(path, format!("{name}\n{}\n", std::process::id()))?;
254 Ok(Self { name, marker: path.to_path_buf(), security, spare: Some(spare) })
255 }
256
257 fn new_instance(&self) -> io::Result<NamedPipeServer> {
258 self.security
259 .create_pipe_instance(ServerOptions::new().reject_remote_clients(true), &self.name)
260 }
261
262 pub async fn accept(&mut self) -> io::Result<DaemonStream> {
263 let server = match self.spare.take() {
264 Some(server) => server,
265 None => self.new_instance()?,
266 };
267 server.connect().await?;
268 self.spare = self.new_instance().ok();
271 Ok(DaemonStream::Server(server))
272 }
273
274 pub fn marker_path(&self) -> &Path {
275 &self.marker
276 }
277 }
278
279 pub async fn connect(path: &Path) -> io::Result<DaemonStream> {
282 let name = pipe_name(path);
283 let deadline = Instant::now() + PIPE_BUSY_RETRY_WINDOW;
284 loop {
285 match ClientOptions::new().open(&name) {
286 Ok(client) => return Ok(DaemonStream::Client(client)),
287 Err(error)
288 if error.raw_os_error() == Some(ERROR_PIPE_BUSY.cast_signed())
289 && Instant::now() < deadline =>
290 {
291 tokio::time::sleep(Duration::from_millis(20)).await;
292 }
293 Err(error) => return Err(error),
294 }
295 }
296 }
297
298 pub fn peer_is_same_user(_stream: &DaemonStream) -> io::Result<bool> {
302 Ok(true)
303 }
304
305 #[cfg(test)]
306 mod tests {
307 use super::pipe_name;
308 use std::path::Path;
309
310 #[test]
311 fn pipe_names_are_stable_and_case_insensitive() {
312 let lower = pipe_name(Path::new(r"c:\users\x\winx\winxd.sock"));
313 let upper = pipe_name(Path::new(r"C:\Users\X\winx\winxd.sock"));
314 assert_eq!(lower, upper);
315 assert!(lower.starts_with(r"\\.\pipe\winx-"));
316 assert_ne!(lower, pipe_name(Path::new(r"C:\Users\X\winx\other.sock")));
317 }
318 }
319}
320
321#[cfg(all(test, unix))]
322mod unix_claim_tests {
323 #![allow(clippy::expect_used)]
324 use super::{connect, lock_path, DaemonListener};
325
326 #[tokio::test]
327 async fn second_bind_fails_while_the_first_listens_and_the_first_stays_reachable() {
328 let dir = tempfile::tempdir().expect("tempdir");
329 let socket = dir.path().join("winxd.sock");
330 let first = DaemonListener::bind(&socket).await.expect("first bind");
331 let error = DaemonListener::bind(&socket)
332 .await
333 .err()
334 .expect("second bind must not steal the socket");
335 assert_eq!(error.kind(), std::io::ErrorKind::AddrInUse, "{error}");
336 assert!(lock_path(&socket).is_file(), "lock file beside the socket");
337 assert!(connect(&socket).await.is_ok(), "first listener still reachable");
339 drop(first);
340 DaemonListener::bind(&socket).await.expect("bind after the first listener is gone");
341 }
342
343 #[tokio::test]
344 async fn stale_socket_from_a_dead_daemon_is_replaced() {
345 let dir = tempfile::tempdir().expect("tempdir");
346 let socket = dir.path().join("winxd.sock");
347 let stale = tokio::net::UnixListener::bind(&socket).expect("stale listener");
348 drop(stale);
349 assert!(socket.exists(), "unix sockets outlive their listener");
350 let listener = DaemonListener::bind(&socket).await.expect("replace stale socket");
351 assert!(connect(&socket).await.is_ok());
352 drop(listener);
353 }
354
355 #[test]
356 fn lock_path_sits_beside_the_socket() {
357 assert_eq!(
358 lock_path(std::path::Path::new("/run/u/winx/winxd.sock")),
359 std::path::PathBuf::from("/run/u/winx/winxd.sock.lock")
360 );
361 }
362}