1use std::io;
5use std::time::{Duration, Instant};
6
7#[cfg(unix)]
16pub type Endpoint = std::path::Path;
17#[cfg(windows)]
19pub type Endpoint = str;
20
21pub const CONNECT_BUDGET: Duration = Duration::from_secs(5);
27
28pub const RETRY_INTERVAL: Duration = Duration::from_millis(50);
30
31pub trait Duplex: Sized + peer::PeerCredentials {
39 type Reader: io::Read + Send + 'static;
41 type Writer: crate::transport::WriteHalf;
43
44 fn connect_once_until(endpoint: &Endpoint, deadline: Instant) -> Result<Self, ConnectError>;
49
50 fn connect_until(endpoint: &Endpoint, deadline: Instant) -> Result<Self, ConnectError> {
56 loop {
57 match Self::connect_once_until(endpoint, deadline) {
58 Ok(connected) => return Ok(connected),
59 Err(error) if !error.is_transient() => return Err(error),
60 Err(error) => {
61 let remaining = deadline.saturating_duration_since(Instant::now());
62 if remaining.is_zero() {
63 return Err(error);
64 }
65 #[expect(
66 clippy::disallowed_methods,
67 reason = "A missing endpoint has no event to await; retry connects within the caller deadline."
68 )]
69 std::thread::sleep(RETRY_INTERVAL.min(remaining));
70 }
71 }
72 }
73 }
74
75 fn connect_once(endpoint: &Endpoint) -> Result<Self, ConnectError> {
77 Self::connect_once_until(endpoint, Instant::now() + CONNECT_BUDGET)
78 }
79
80 fn connect(endpoint: &Endpoint) -> Result<Self, ConnectError> {
82 Self::connect_until(endpoint, Instant::now() + CONNECT_BUDGET)
83 }
84
85 fn set_read_deadline(&self, timeout: Option<Duration>) -> io::Result<()>;
87
88 fn split(self) -> io::Result<(Self::Reader, Self::Writer)>;
90}
91
92#[derive(Debug)]
99pub enum Bound<L> {
100 Won(L),
102 AlreadyRunning,
104}
105
106#[cfg(unix)]
111pub use unix::UnixDuplex as PlatformDuplex;
112#[cfg(windows)]
114pub use windows::WindowsDuplex as PlatformDuplex;
115
116pub(crate) use process::wait_child;
117pub use process::{ProcessHandle, process_has_exited, process_state};
118#[cfg(all(unix, feature = "async"))]
122pub use unix::shutdown_signal;
123#[cfg(unix)]
124pub use unix::terminate_legacy_peer;
125#[cfg(all(windows, feature = "async"))]
126pub use windows::shutdown_signal;
127#[cfg(windows)]
128pub use windows::terminate_legacy_peer;
129
130#[derive(Clone, Copy, Debug, PartialEq, Eq)]
145#[non_exhaustive]
146pub enum ProcessState {
147 Alive,
150 Exited,
152 Unknown,
155}
156
157#[cfg(unix)]
165pub use unix_socket as socket;
166#[cfg(windows)]
168pub use windows_socket as socket;
169
170#[derive(Clone, Copy, Debug, PartialEq, Eq)]
172#[non_exhaustive]
173pub enum ConnectError {
174 ConnectTimeout,
177 PermissionDenied,
179 EndpointTooLong,
182 Failed(io::ErrorKind),
184}
185impl std::fmt::Display for ConnectError {
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 match self {
188 Self::ConnectTimeout => f.write_str("daemon connection timed out"),
189 Self::PermissionDenied => f.write_str("daemon endpoint permission denied"),
190 Self::EndpointTooLong => {
191 f.write_str("daemon endpoint path is too long for a Unix socket")
192 }
193 Self::Failed(kind) => write!(f, "daemon connection failed: {kind}"),
194 }
195 }
196}
197impl std::error::Error for ConnectError {}
198impl ConnectError {
199 pub fn is_transient(&self) -> bool {
203 matches!(self, Self::ConnectTimeout)
204 }
205}
206
207pub fn is_transient(error: &io::Error) -> bool {
218 use io::ErrorKind::{
219 BrokenPipe, ConnectionAborted, ConnectionRefused, ConnectionReset, Interrupted,
220 NotConnected, NotFound, TimedOut, UnexpectedEof, WouldBlock,
221 };
222 matches!(
223 error.kind(),
224 NotFound
225 | ConnectionRefused
226 | ConnectionReset
227 | ConnectionAborted
228 | NotConnected
229 | UnexpectedEof
230 | BrokenPipe
231 | TimedOut
232 | WouldBlock
233 | Interrupted
234 ) || busy_pipe(error)
235}
236
237#[cfg(windows)]
238use windows::busy_pipe;
239#[cfg(unix)]
242fn busy_pipe(_: &io::Error) -> bool {
243 false
244}
245
246pub(crate) fn connect_error(error: &io::Error) -> ConnectError {
248 if error.kind() == io::ErrorKind::PermissionDenied {
249 ConnectError::PermissionDenied
250 } else if is_transient(error) {
251 ConnectError::ConnectTimeout
252 } else {
253 ConnectError::Failed(error.kind())
254 }
255}
256
257pub mod peer;
258mod process;
259#[cfg(unix)]
260pub mod unix;
261#[cfg(unix)]
262pub mod unix_socket;
263#[cfg(windows)]
264pub mod windows;
265
266#[cfg(test)]
267fn test_handshake(write: &mut impl io::Write, read: &mut impl io::Read, _: &str) -> io::Result<()> {
268 write.write_all(b"hello\n")?;
269 write.flush()?;
270 let mut reply = [0; 6];
271 read.read_exact(&mut reply)?;
272 if reply != *b"ready\n" {
273 return Err(io::Error::other("invalid test reply"));
274 }
275 Ok(())
276}
277
278#[cfg(windows)]
279pub mod windows_socket;
280
281#[cfg(windows)]
282pub mod pipe_name;
283#[cfg(all(test, not(windows)))]
286mod pipe_name;
287
288#[cfg(any(all(windows, feature = "launch"), test))]
289pub(crate) mod command_line;
290#[cfg(all(windows, feature = "launch"))]
291pub(crate) mod windows_spawn;
292
293#[cfg(test)]
294mod connect_error_tests {
295 use super::{ConnectError, connect_error};
296 use std::io::{self, ErrorKind};
297
298 #[test]
299 fn an_absent_or_departing_peer_is_worth_retrying() {
300 for kind in [
304 ErrorKind::NotFound,
305 ErrorKind::ConnectionRefused,
306 ErrorKind::TimedOut,
307 ErrorKind::WouldBlock,
308 ErrorKind::Interrupted,
309 ErrorKind::ConnectionReset,
310 ErrorKind::ConnectionAborted,
311 ErrorKind::NotConnected,
312 ErrorKind::UnexpectedEof,
313 ErrorKind::BrokenPipe,
314 ] {
315 assert!(super::is_transient(&kind.into()), "{kind:?}");
316 let error = connect_error(&kind.into());
317 assert_eq!(error, ConnectError::ConnectTimeout, "{kind:?}");
318 assert!(error.is_transient());
319 }
320 for kind in [
321 ErrorKind::PermissionDenied,
322 ErrorKind::InvalidInput,
323 ErrorKind::InvalidData,
324 ErrorKind::AddrInUse,
325 ErrorKind::Other,
326 ] {
327 assert!(!super::is_transient(&kind.into()), "{kind:?}");
328 }
329 assert_eq!(
330 connect_error(&ErrorKind::PermissionDenied.into()),
331 ConnectError::PermissionDenied
332 );
333 let other = connect_error(&ErrorKind::InvalidInput.into());
334 assert_eq!(other, ConnectError::Failed(ErrorKind::InvalidInput));
335 assert!(!other.is_transient());
336 assert!(!ConnectError::PermissionDenied.is_transient());
337 assert!(!ConnectError::EndpointTooLong.is_transient());
338 }
339
340 #[test]
341 fn a_busy_pipe_is_retried_only_on_windows() {
342 let busy = connect_error(&io::Error::from_raw_os_error(231));
343 if cfg!(windows) {
344 assert_eq!(busy, ConnectError::ConnectTimeout);
345 } else {
346 assert_ne!(busy, ConnectError::ConnectTimeout);
347 }
348 assert_eq!(
349 super::is_transient(&io::Error::from_raw_os_error(231)),
350 cfg!(windows)
351 );
352 }
353
354 #[test]
355 fn a_temporary_connection_failure_is_retried_until_the_endpoint_accepts() {
356 use super::{Duplex, Endpoint, PlatformDuplex, peer::PeerCredentials};
357 use std::sync::atomic::{AtomicUsize, Ordering};
358 static ATTEMPTS: AtomicUsize = AtomicUsize::new(0);
359 struct StartsAfterOneAttempt;
360 impl PeerCredentials for StartsAfterOneAttempt {
361 fn credentials(&self) -> io::Result<crate::peer::Credentials> {
362 unreachable!()
363 }
364 }
365 impl Duplex for StartsAfterOneAttempt {
366 type Reader = <PlatformDuplex as Duplex>::Reader;
367 type Writer = <PlatformDuplex as Duplex>::Writer;
368 fn connect_once_until(
369 _: &Endpoint,
370 _: std::time::Instant,
371 ) -> Result<Self, ConnectError> {
372 if ATTEMPTS.fetch_add(1, Ordering::Relaxed) == 0 {
373 Err(ConnectError::ConnectTimeout)
374 } else {
375 Ok(Self)
376 }
377 }
378 fn set_read_deadline(&self, _: Option<std::time::Duration>) -> io::Result<()> {
379 unreachable!()
380 }
381 fn split(self) -> io::Result<(Self::Reader, Self::Writer)> {
382 unreachable!()
383 }
384 }
385 #[cfg(unix)]
386 let endpoint = std::path::Path::new("unused");
387 #[cfg(windows)]
388 let endpoint = "unused";
389 assert!(
390 StartsAfterOneAttempt::connect_until(
391 endpoint,
392 std::time::Instant::now() + std::time::Duration::from_secs(1)
393 )
394 .is_ok()
395 );
396 assert_eq!(ATTEMPTS.load(Ordering::Relaxed), 2);
397 }
398
399 #[test]
400 fn every_failure_says_what_happened() {
401 let cases = [
402 (ConnectError::ConnectTimeout, "daemon connection timed out"),
403 (
404 ConnectError::PermissionDenied,
405 "daemon endpoint permission denied",
406 ),
407 (
408 ConnectError::EndpointTooLong,
409 "daemon endpoint path is too long for a Unix socket",
410 ),
411 (
412 ConnectError::Failed(ErrorKind::InvalidInput),
413 "daemon connection failed: invalid input parameter",
414 ),
415 ];
416 for (error, text) in cases {
417 assert_eq!(error.to_string(), text);
418 }
419 }
420}
421
422mod readiness;