1use std::cell::Cell;
4use std::io::{self, Read, Write};
5use std::net::Shutdown;
6use std::os::unix::net::UnixStream;
7use std::sync::{
8 Arc, Mutex,
9 atomic::{AtomicBool, Ordering},
10};
11use std::time::{Duration, Instant};
12
13use super::{ConnectError, Endpoint};
14use crate::local::Duplex;
15use crate::transport::WriteHalf;
16
17pub struct UnixDuplex {
19 stream: UnixStream,
20 read_deadline: Cell<Option<Instant>>,
21}
22
23#[cfg(target_os = "linux")]
24const CHILD_SIGNAL: std::os::raw::c_int = 17;
25#[cfg(not(target_os = "linux"))]
26const CHILD_SIGNAL: std::os::raw::c_int = 20;
27
28fn child_disposition(handler: usize) -> io::Result<usize> {
29 unsafe extern "C" {
30 fn signal(number: std::os::raw::c_int, handler: usize) -> usize;
31 }
32 let previous = unsafe { signal(CHILD_SIGNAL, handler) };
37 if previous == usize::MAX {
38 Err(io::Error::last_os_error())
39 } else {
40 Ok(previous)
41 }
42}
43
44pub fn auto_reap_children() -> io::Result<()> {
47 child_disposition(1).map(|_| ())
48}
49
50pub fn restore_child_waiting() -> io::Result<()> {
53 child_disposition(0).map(|_| ())
54}
55
56pub fn spawn_with_child_waiting(
66 command: &mut std::process::Command,
67) -> io::Result<std::process::Child> {
68 spawn_then(command, |_| Ok(())).map(|(child, ())| child)
69}
70
71fn spawn_then<T>(
79 command: &mut std::process::Command,
80 watch: impl FnOnce(&std::process::Child) -> io::Result<T>,
81) -> io::Result<(std::process::Child, T)> {
82 let _spawn = crate::spawn_lock::spawning();
85 let previous = child_disposition(0)?;
86 let child = command.spawn().and_then(|child| watched(child, watch));
87 child_disposition(previous)?;
88 if previous == 1 {
89 reap_exited_children();
90 }
91 child
92}
93
94fn watched<T>(
95 mut child: std::process::Child,
96 watch: impl FnOnce(&std::process::Child) -> io::Result<T>,
97) -> io::Result<(std::process::Child, T)> {
98 match watch(&child) {
99 Ok(watching) => Ok((child, watching)),
100 Err(error) => {
101 let _ = child.kill();
102 let _ = child.wait();
103 Err(error)
104 }
105 }
106}
107
108fn reap_exited_children() {
109 unsafe extern "C" {
110 fn waitpid(
111 pid: std::os::raw::c_int,
112 status: *mut std::os::raw::c_int,
113 options: std::os::raw::c_int,
114 ) -> std::os::raw::c_int;
115 }
116 loop {
119 let pid = unsafe { waitpid(-1, std::ptr::null_mut(), 1) };
124 if pid > 0 {
125 continue;
126 }
127 if pid == -1 && io::Error::last_os_error().kind() == io::ErrorKind::Interrupted {
128 continue;
129 }
130 break;
131 }
132}
133
134use std::os::fd::{AsRawFd, RawFd};
135
136#[cfg(target_os = "macos")]
138fn macos_peer_pid(fd: RawFd) -> io::Result<u32> {
139 use std::os::raw::{c_int, c_void};
140 unsafe extern "C" {
141 fn getsockopt(
142 socket: c_int,
143 level: c_int,
144 name: c_int,
145 value: *mut c_void,
146 len: *mut u32,
147 ) -> c_int;
148 }
149 const SOL_LOCAL: c_int = 0;
150 const LOCAL_PEERPID: c_int = 0x002;
151 let mut pid: c_int = 0;
152 let mut len = std::mem::size_of::<c_int>() as u32;
153 let rc = unsafe {
156 getsockopt(
157 fd,
158 SOL_LOCAL,
159 LOCAL_PEERPID,
160 (&raw mut pid).cast(),
161 &raw mut len,
162 )
163 };
164 if rc == 0 && pid > 0 {
165 Ok(pid as u32)
166 } else {
167 Err(io::Error::last_os_error())
168 }
169}
170
171#[cfg(not(target_os = "macos"))]
174pub(crate) fn fd_credentials(fd: RawFd) -> io::Result<crate::peer::Credentials> {
175 let (pid, uid) = peer_credentials(fd)?;
176 Ok(crate::peer::Credentials::new(
177 pid.and_then(crate::ProcessId::new),
178 uid == own_uid(),
179 ))
180}
181
182#[cfg(target_os = "macos")]
185pub(crate) fn macos_credentials(fd: RawFd) -> io::Result<crate::peer::Credentials> {
186 let same_user = macos_peer_uid(fd)? == own_uid();
187 let pid = crate::ProcessId::new(macos_peer_pid(fd)?);
188 Ok(crate::peer::Credentials::new(pid, same_user))
189}
190
191#[cfg(target_os = "macos")]
192pub(crate) use macos_credentials as fd_credentials;
193
194#[cfg(not(target_os = "macos"))]
196fn peer_credentials(fd: RawFd) -> io::Result<(Option<u32>, u32)> {
197 use std::os::raw::{c_int, c_void};
198 #[repr(C)]
199 struct UCred {
200 pid: c_int,
201 uid: u32,
202 gid: u32,
203 }
204 unsafe extern "C" {
205 fn getsockopt(
206 socket: c_int,
207 level: c_int,
208 name: c_int,
209 value: *mut c_void,
210 len: *mut u32,
211 ) -> c_int;
212 }
213 const SOL_SOCKET: c_int = 1;
214 const SO_PEERCRED: c_int = 17;
215 let mut cred = UCred {
216 pid: 0,
217 uid: 0,
218 gid: 0,
219 };
220 let mut len = std::mem::size_of::<UCred>() as u32;
221 let rc = unsafe {
224 getsockopt(
225 fd,
226 SOL_SOCKET,
227 SO_PEERCRED,
228 (&raw mut cred).cast(),
229 &raw mut len,
230 )
231 };
232 if rc != 0 {
233 return Err(io::Error::last_os_error());
234 }
235 Ok((visible_pid(cred.pid), cred.uid))
236}
237
238#[cfg(not(target_os = "macos"))]
241fn visible_pid(raw: std::os::raw::c_int) -> Option<u32> {
242 u32::try_from(raw).ok().filter(|pid| *pid > 0)
243}
244
245pub(crate) fn own_uid() -> u32 {
247 unsafe extern "C" {
248 fn geteuid() -> u32;
249 }
250 unsafe { geteuid() }
254}
255
256pub(crate) fn open_private(path: &std::path::Path) -> io::Result<std::fs::File> {
261 use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
262 let file = std::fs::OpenOptions::new()
263 .read(true)
264 .write(true)
265 .create(true)
266 .truncate(false)
267 .mode(0o600)
268 .custom_flags(libc::O_NOFOLLOW)
269 .open(path)
270 .map_err(|error| {
271 if error.raw_os_error() == Some(libc::ELOOP) {
273 io::ErrorKind::PermissionDenied.into()
274 } else {
275 error
276 }
277 })?;
278 if file.metadata()?.uid() != own_uid() {
279 return Err(io::ErrorKind::PermissionDenied.into());
280 }
281 file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
282 Ok(file)
283}
284
285#[cfg(target_os = "macos")]
287fn macos_peer_uid(fd: RawFd) -> io::Result<u32> {
288 unsafe extern "C" {
289 fn getpeereid(fd: RawFd, uid: *mut u32, gid: *mut u32) -> i32;
290 }
291 let (mut uid, mut gid) = (0, 0);
292 if unsafe { getpeereid(fd, &mut uid, &mut gid) } != 0 {
294 return Err(io::Error::last_os_error());
295 }
296 Ok(uid)
297}
298
299pub(crate) fn signal_state(pid: u32) -> super::ProcessState {
305 use super::ProcessState;
306 unsafe extern "C" {
307 fn kill(pid: i32, signal: i32) -> i32;
308 }
309 if pid == 0 || pid > i32::MAX as u32 {
310 return ProcessState::Unknown;
311 }
312 #[cfg(target_os = "linux")]
315 if std::fs::read_to_string(format!("/proc/{pid}/stat")).is_ok_and(|stat| {
316 stat.rsplit_once(") ")
317 .is_some_and(|(_, fields)| fields.starts_with("Z ") || fields.starts_with("X "))
318 }) {
319 return ProcessState::Exited;
320 }
321 if unsafe { kill(pid as i32, 0) } == 0 {
323 return ProcessState::Alive;
324 }
325 liveness_from_errno(io::Error::last_os_error().raw_os_error())
326}
327
328fn liveness_from_errno(errno: Option<i32>) -> super::ProcessState {
331 use super::ProcessState;
332 const EPERM: i32 = 1;
333 const ESRCH: i32 = 3;
334 match errno {
335 Some(ESRCH) => ProcessState::Exited,
336 Some(EPERM) => ProcessState::Alive,
337 _ => ProcessState::Unknown,
338 }
339}
340
341#[cfg(all(test, feature = "launch"))]
344pub(crate) fn interrupt_process_group(pgid: u32) -> io::Result<()> {
345 unsafe extern "C" {
346 fn kill(pid: i32, signal: i32) -> i32;
347 }
348 const SIGINT: i32 = 2;
349 let pgid = i32::try_from(pgid).map_err(|_| io::ErrorKind::InvalidInput)?;
350 if unsafe { kill(-pgid, SIGINT) } == 0 {
353 Ok(())
354 } else {
355 Err(io::Error::last_os_error())
356 }
357}
358
359pub fn spawn_in_new_session(
379 command: &mut std::process::Command,
380) -> io::Result<std::process::Child> {
381 in_new_session(command, None);
382 spawn_with_child_waiting(command)
383}
384
385#[cfg(feature = "launch")]
389pub(crate) fn spawn_in_new_session_watched(
390 command: &mut std::process::Command,
391 keep: Option<RawFd>,
392) -> io::Result<(std::process::Child, super::ProcessHandle)> {
393 in_new_session(command, keep);
394 spawn_then(command, super::ProcessHandle::for_child)
395}
396
397fn in_new_session(command: &mut std::process::Command, keep: Option<RawFd>) {
400 use std::os::unix::process::CommandExt;
401 let descriptor_limit = descriptor_limit();
404 let prepare = move || {
405 if unsafe { libc::setsid() } == -1 {
409 return Err(io::Error::last_os_error());
410 }
411 mark_inherited_descriptors_cloexec(descriptor_limit);
412 if let Some(fd) = keep {
413 checked(unsafe { libc::fcntl(fd, libc::F_SETFD, 0) })?;
417 }
418 Ok(())
419 };
420 unsafe {
425 command.pre_exec(prepare);
426 }
427}
428
429fn checked(result: libc::c_int) -> io::Result<libc::c_int> {
433 if result == -1 {
434 Err(io::Error::last_os_error())
435 } else {
436 Ok(result)
437 }
438}
439
440#[cfg(feature = "launch")]
461pub(crate) fn ready_pipe() -> io::Result<(std::io::PipeReader, std::os::fd::OwnedFd)> {
462 use std::os::fd::{FromRawFd, OwnedFd};
463 let (reader, writer) = crate::spawn_lock::without_spawns(std::io::pipe)?;
464 let writer = OwnedFd::from(writer);
465 let raised = checked(unsafe { libc::fcntl(writer.as_raw_fd(), libc::F_DUPFD_CLOEXEC, 3) })?;
468 Ok((reader, unsafe { OwnedFd::from_raw_fd(raised) }))
470}
471
472pub(crate) unsafe fn take_ready_descriptor(value: &str) -> io::Result<std::io::PipeWriter> {
486 use std::os::fd::{FromRawFd, OwnedFd};
487 let refuse = |message: &'static str| io::Error::new(io::ErrorKind::InvalidInput, message);
488 let fd: RawFd = value
489 .parse()
490 .map_err(|_| refuse("the readiness channel is not a descriptor number"))?;
491 if fd <= 2 {
492 return Err(refuse("a standard stream is not a readiness channel"));
493 }
494 let mut status = std::mem::MaybeUninit::<libc::stat>::uninit();
495 checked(unsafe { libc::fstat(fd, status.as_mut_ptr()) })?;
498 let status = unsafe { status.assume_init() };
500 if (status.st_mode & libc::S_IFMT) != libc::S_IFIFO {
501 return Err(refuse("the readiness channel is not a pipe"));
502 }
503 let flags = checked(unsafe { libc::fcntl(fd, libc::F_GETFL) })?;
505 if (flags & libc::O_ACCMODE) != libc::O_WRONLY {
506 return Err(refuse(
507 "the readiness channel is not the write end of a pipe",
508 ));
509 }
510 checked(unsafe { libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC) })?;
513 Ok(std::io::PipeWriter::from(unsafe {
516 OwnedFd::from_raw_fd(fd)
517 }))
518}
519
520fn descriptor_limit() -> libc::c_int {
526 const CAP: libc::rlim_t = 1 << 16;
527 let mut limit = libc::rlimit {
528 rlim_cur: 0,
529 rlim_max: 0,
530 };
531 let known = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) } == 0;
533 let current = if known { limit.rlim_cur } else { 1024 };
534 libc::c_int::try_from(current.min(CAP)).unwrap_or(1 << 16)
535}
536
537#[cfg(all(test, feature = "launch"))]
540pub(crate) fn inheritable_pipe() -> (std::fs::File, std::fs::File) {
541 use std::os::fd::FromRawFd;
542 let mut fds = [0; 2];
543 assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
545 unsafe {
547 (
548 std::fs::File::from_raw_fd(fds[0]),
549 std::fs::File::from_raw_fd(fds[1]),
550 )
551 }
552}
553
554fn mark_inherited_descriptors_cloexec(limit: libc::c_int) {
556 #[cfg(target_os = "linux")]
557 {
558 const CLOSE_RANGE_CLOEXEC: libc::c_uint = 1 << 2;
559 let done = unsafe {
564 libc::syscall(
565 libc::SYS_close_range,
566 3 as libc::c_uint,
567 libc::c_uint::MAX,
568 CLOSE_RANGE_CLOEXEC,
569 )
570 } == 0;
571 if done {
572 return;
573 }
574 }
575 for fd in 3..limit {
576 unsafe {
580 libc::fcntl(fd, libc::F_SETFD, libc::FD_CLOEXEC);
581 }
582 }
583}
584
585pub fn terminate_legacy_peer(pid: crate::ProcessId) -> io::Result<()> {
592 use std::os::raw::c_int;
593 unsafe extern "C" {
594 fn kill(pid: c_int, signal: c_int) -> c_int;
595 }
596 const SIGTERM: c_int = 15;
597 let pid = c_int::try_from(pid.get())
599 .map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "PID out of range"))?;
600 if unsafe { kill(pid, SIGTERM) } == 0 {
603 Ok(())
604 } else {
605 Err(io::Error::last_os_error())
606 }
607}
608
609#[cfg(feature = "async")]
629pub fn shutdown_signal() -> io::Result<impl Future<Output = ()> + Send + 'static> {
630 use tokio::signal::unix::{SignalKind, signal};
631 let mut terminate = signal(SignalKind::terminate())?;
632 let mut interrupt = signal(SignalKind::interrupt())?;
633 Ok(async move {
634 tokio::select! {
635 _ = terminate.recv() => {}
636 _ = interrupt.recv() => {}
637 }
638 })
639}
640
641impl super::peer::PeerCredentials for UnixDuplex {
642 fn credentials(&self) -> io::Result<crate::peer::Credentials> {
643 fd_credentials(self.stream.as_raw_fd())
644 }
645}
646
647impl Duplex for UnixDuplex {
648 type Reader = UnixReader;
649 type Writer = UnixWriter;
650
651 fn connect_once_until(path: &Endpoint, deadline: Instant) -> Result<Self, ConnectError> {
653 if crate::socket_path::check_socket_path(path).is_err() {
656 return Err(ConnectError::EndpointTooLong);
657 }
658 let connect = || -> io::Result<UnixStream> {
659 let remaining = deadline.saturating_duration_since(Instant::now());
660 if remaining.is_zero() {
661 return Err(io::ErrorKind::TimedOut.into());
662 }
663 let socket = socket2::Socket::new(socket2::Domain::UNIX, socket2::Type::STREAM, None)?;
664 socket.connect_timeout(&socket2::SockAddr::unix(path)?, remaining)?;
665 let fd: std::os::fd::OwnedFd = socket.into();
666 let stream = UnixStream::from(fd);
667 stream.peer_addr()?;
668 Ok(stream)
669 };
670 connect()
671 .map(|stream| Self {
672 stream,
673 read_deadline: Cell::new(None),
674 })
675 .map_err(|error| super::connect_error(&error))
676 }
677
678 fn set_read_deadline(&self, timeout: Option<Duration>) -> io::Result<()> {
680 let deadline = timeout
681 .map(|duration| {
682 Instant::now().checked_add(duration).ok_or_else(|| {
683 io::Error::new(io::ErrorKind::InvalidInput, "read deadline overflow")
684 })
685 })
686 .transpose()?;
687 self.stream.set_nonblocking(deadline.is_some())?;
688 self.read_deadline.set(deadline);
689 Ok(())
690 }
691
692 fn split(self) -> io::Result<(Self::Reader, Self::Writer)> {
693 let deadline = self.read_deadline.get();
694 let writer = self.stream.try_clone()?;
695 let setup = Arc::new(Setup {
696 active: AtomicBool::new(deadline.is_some()),
697 transition: Mutex::new(()),
698 });
699 Ok((
700 UnixReader {
701 stream: self.stream,
702 read_deadline: deadline,
703 setup: Arc::clone(&setup),
704 },
705 UnixWriter {
706 stream: writer,
707 deadline,
708 setup,
709 },
710 ))
711 }
712}
713
714struct Setup {
717 active: AtomicBool,
718 transition: Mutex<()>,
719}
720
721pub struct UnixReader {
723 stream: UnixStream,
724 read_deadline: Option<Instant>,
725 setup: Arc<Setup>,
726}
727
728impl UnixReader {
729 pub fn clear_read_deadline(&mut self) -> io::Result<()> {
731 let _transition = self
732 .setup
733 .transition
734 .lock()
735 .unwrap_or_else(|error| error.into_inner());
736 self.stream.set_nonblocking(false)?;
737 self.read_deadline = None;
738 self.setup.active.store(false, Ordering::Release);
739 Ok(())
740 }
741}
742
743impl Read for UnixReader {
744 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
745 let Some(deadline) = self.read_deadline else {
746 return self.stream.read(buf);
747 };
748 read_until(&self.stream, buf, deadline)
749 }
750}
751
752fn establishment_timeout() -> io::Error {
753 io::Error::new(
754 io::ErrorKind::TimedOut,
755 "Unix-socket session establishment timed out",
756 )
757}
758
759pub struct UnixWriter {
761 stream: UnixStream,
762 deadline: Option<Instant>,
763 setup: Arc<Setup>,
764}
765
766impl Write for UnixWriter {
767 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
768 if !self.setup.active.load(Ordering::Acquire) {
769 return self.stream.write(buf);
770 }
771 let transition = self
772 .setup
773 .transition
774 .lock()
775 .unwrap_or_else(|error| error.into_inner());
776 if self.setup.active.load(Ordering::Acquire) {
777 let deadline = self.deadline.expect("setup has a deadline");
778 write_until(&self.stream, buf, deadline)
779 } else {
780 drop(transition);
781 self.stream.write(buf)
782 }
783 }
784 fn flush(&mut self) -> io::Result<()> {
785 self.stream.flush()
786 }
787}
788
789impl WriteHalf for UnixWriter {
790 fn shutdown_write(&mut self) -> io::Result<()> {
791 self.stream.shutdown(Shutdown::Write)
792 }
793}
794
795impl Read for UnixDuplex {
796 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
797 match self.read_deadline.get() {
798 Some(deadline) => read_until(&self.stream, buf, deadline),
799 None => self.stream.read(buf),
800 }
801 }
802}
803
804impl Write for UnixDuplex {
805 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
806 match self.read_deadline.get() {
807 Some(deadline) => write_until(&self.stream, buf, deadline),
808 None => self.stream.write(buf),
809 }
810 }
811 fn flush(&mut self) -> io::Result<()> {
812 self.stream.flush()
813 }
814}
815
816fn read_until(stream: &UnixStream, bytes: &mut [u8], deadline: Instant) -> io::Result<usize> {
817 if bytes.is_empty() {
818 return Ok(0);
819 }
820 let fd = stream.as_raw_fd();
821 bounded_io(fd, libc::POLLIN, deadline, || {
822 unsafe {
824 libc::recv(
825 fd,
826 bytes.as_mut_ptr().cast(),
827 bytes.len(),
828 libc::MSG_DONTWAIT,
829 )
830 }
831 })
832}
833fn write_until(stream: &UnixStream, bytes: &[u8], deadline: Instant) -> io::Result<usize> {
834 if bytes.is_empty() {
835 return Ok(0);
836 }
837 let fd = stream.as_raw_fd();
838 bounded_io(fd, libc::POLLOUT, deadline, || {
839 unsafe {
843 libc::send(
844 fd,
845 bytes.as_ptr().cast(),
846 bytes.len(),
847 libc::MSG_DONTWAIT | libc::MSG_NOSIGNAL,
848 )
849 }
850 })
851}
852fn bounded_io(
853 fd: RawFd,
854 events: libc::c_short,
855 deadline: Instant,
856 mut operation: impl FnMut() -> isize,
857) -> io::Result<usize> {
858 loop {
859 if Instant::now() >= deadline {
860 return Err(establishment_timeout());
861 }
862 let count = operation();
863 if count >= 0 {
864 if Instant::now() >= deadline {
865 return Err(establishment_timeout());
866 }
867 return Ok(count as usize);
868 }
869 let error = io::Error::last_os_error();
870 match error.kind() {
871 io::ErrorKind::Interrupted => continue,
872 io::ErrorKind::WouldBlock => {}
873 _ => return Err(error),
874 }
875 let remaining = deadline.saturating_duration_since(Instant::now());
876 let millis = remaining.as_millis().max(1).min(i32::MAX as u128) as i32;
877 let mut poll = libc::pollfd {
878 fd,
879 events,
880 revents: 0,
881 };
882 let result = unsafe { libc::poll(&mut poll, 1, millis) };
884 if result < 0 {
885 let error = io::Error::last_os_error();
886 if error.kind() != io::ErrorKind::Interrupted {
887 return Err(error);
888 }
889 }
890 }
892}
893
894#[cfg(test)]
895mod tests {
896 #[test]
897 fn only_minus_one_is_a_failed_call() {
898 assert_eq!(super::checked(0).unwrap(), 0);
899 assert_eq!(super::checked(1).unwrap(), 1);
900 assert_eq!(super::checked(3).unwrap(), 3);
901 assert!(super::checked(-1).is_err());
902 }
903
904 use super::*;
905 use crate::local::peer::PeerCredentials as _;
906 use std::os::unix::net::UnixListener;
907
908 #[test]
909 fn a_closed_peer_does_not_hide_the_buffered_final_response() {
910 let (stream, mut peer) = UnixStream::pair().unwrap();
911 let duplex = UnixDuplex {
912 stream,
913 read_deadline: Cell::new(None),
914 };
915 duplex
916 .set_read_deadline(Some(Duration::from_secs(1)))
917 .unwrap();
918 let (mut read, _write) = duplex.split().unwrap();
919 peer.write_all(b"final response").unwrap();
920 drop(peer);
921 let mut bytes = Vec::new();
922 read.read_to_end(&mut bytes).unwrap();
923 assert_eq!(bytes, b"final response");
924 read.clear_read_deadline().unwrap();
925 }
926
927 #[test]
928 fn an_unsplit_socket_also_honors_its_setup_deadline() {
929 let (stream, _peer) = UnixStream::pair().unwrap();
930 let mut duplex = UnixDuplex {
931 stream,
932 read_deadline: Cell::new(None),
933 };
934 duplex
935 .set_read_deadline(Some(Duration::from_millis(10)))
936 .unwrap();
937 let started = Instant::now();
938 assert_eq!(
939 duplex.read(&mut [0]).unwrap_err().kind(),
940 io::ErrorKind::TimedOut
941 );
942 assert!(started.elapsed() < Duration::from_secs(1));
943 }
944
945 #[test]
946 #[ignore = "child fixture for legacy peer termination"]
947 fn legacy_peer_waits_for_input() {
948 let mut byte = [0];
949 std::io::stdin().read_exact(&mut byte).unwrap();
950 }
951
952 #[test]
953 fn legacy_termination_signals_only_its_child_and_reports_os_failures() {
954 let child = std::process::Command::new(std::env::current_exe().unwrap())
955 .args([
956 "--exact",
957 "local::unix::tests::legacy_peer_waits_for_input",
958 "--ignored",
959 ])
960 .stdin(std::process::Stdio::piped())
961 .stdout(std::process::Stdio::null())
962 .stderr(std::process::Stdio::null())
963 .spawn()
964 .unwrap();
965 let mut child = crate::Candidate::new(child);
966 terminate_legacy_peer(child.id()).unwrap();
967 let status = child
968 .wait_until(Instant::now() + Duration::from_secs(10))
969 .unwrap()
970 .unwrap();
971 use std::os::unix::process::ExitStatusExt;
972 assert_eq!(status.signal(), Some(15));
973 let missing = crate::ProcessId::new(i32::MAX as u32).unwrap();
974 assert_eq!(
975 terminate_legacy_peer(missing).unwrap_err().raw_os_error(),
976 Some(3)
977 );
978 }
979
980 #[test]
981 fn terminating_a_pid_outside_the_process_range_is_refused() {
982 let error = terminate_legacy_peer(crate::ProcessId::new(u32::MAX).unwrap()).unwrap_err();
983 assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
984 }
985
986 #[test]
987 fn process_state_reports_alive_exited_and_unknown() {
988 use super::super::{ProcessState, process_has_exited, process_state};
989 use crate::ProcessId;
990 assert_eq!(process_state(ProcessId::current()), ProcessState::Alive);
991 assert_eq!(
992 process_state(ProcessId::new(u32::MAX).unwrap()),
993 ProcessState::Unknown
994 );
995 let mut child = std::process::Command::new("true").spawn().unwrap();
996 let pid = ProcessId::of(&child);
997 child.wait().unwrap();
998 assert_eq!(process_state(pid), ProcessState::Exited);
1000 assert!(process_has_exited(pid));
1001 }
1002
1003 fn reclaim(fd: RawFd) {
1005 use std::os::fd::{FromRawFd, OwnedFd};
1006 drop(unsafe { OwnedFd::from_raw_fd(fd) });
1008 }
1009
1010 #[test]
1011 fn only_the_write_end_of_a_pipe_is_taken_as_a_readiness_channel() {
1012 use std::os::fd::{IntoRawFd, OwnedFd};
1013 for value in ["", "x", "-1", "0", "1", "2"] {
1014 let error = unsafe { take_ready_descriptor(value) }.unwrap_err();
1016 assert_eq!(error.kind(), io::ErrorKind::InvalidInput, "{value:?}");
1017 }
1018
1019 let file = tempfile::tempfile().unwrap().into_raw_fd();
1020 let error = unsafe { take_ready_descriptor(&file.to_string()) }.unwrap_err();
1022 assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
1023 reclaim(file);
1024
1025 let (reader, writer) = std::io::pipe().unwrap();
1026 let read_end = OwnedFd::from(reader).into_raw_fd();
1027 let error = unsafe { take_ready_descriptor(&read_end.to_string()) }.unwrap_err();
1029 assert_eq!(error.kind(), io::ErrorKind::InvalidInput);
1030 reclaim(read_end);
1031 drop(writer);
1032 }
1033
1034 #[test]
1035 fn a_taken_readiness_channel_is_not_inherited_and_carries_messages() {
1036 use std::os::fd::{IntoRawFd, OwnedFd};
1037 let (mut reader, writer) = std::io::pipe().unwrap();
1038 let write_end = OwnedFd::from(writer);
1039 let cleared = unsafe { libc::fcntl(write_end.as_raw_fd(), libc::F_SETFD, 0) };
1042 assert_ne!(cleared, -1);
1043 let write_end = write_end.into_raw_fd();
1044 let mut pipe = unsafe { take_ready_descriptor(&write_end.to_string()) }.unwrap();
1046 let flags = unsafe { libc::fcntl(write_end, libc::F_GETFD) };
1048 assert_ne!(
1049 flags & libc::FD_CLOEXEC,
1050 0,
1051 "programs the daemon starts would hold it"
1052 );
1053 pipe.write_all(b"ready\n").unwrap();
1054 let mut received = [0u8; 6];
1055 reader.read_exact(&mut received).unwrap();
1056 assert_eq!(&received, b"ready\n");
1057 }
1058
1059 #[cfg(feature = "launch")]
1060 #[test]
1061 fn a_readiness_pipe_leaves_the_standard_streams_alone() {
1062 let (_reader, child_end) = ready_pipe().unwrap();
1063 let fd = child_end.as_raw_fd();
1064 assert!(fd > 2, "{fd}");
1065 let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
1067 assert_ne!(
1068 flags & libc::FD_CLOEXEC,
1069 0,
1070 "another child could inherit it"
1071 );
1072 }
1073
1074 #[test]
1075 fn the_signal_fallback_reports_alive_exited_and_unknown() {
1076 use super::super::ProcessState;
1077 assert_eq!(signal_state(std::process::id()), ProcessState::Alive);
1078 assert_eq!(signal_state(0), ProcessState::Unknown);
1079 assert_eq!(signal_state(u32::MAX), ProcessState::Unknown);
1080 let mut child = std::process::Command::new("true").spawn().unwrap();
1081 let pid = child.id();
1082 child.wait().unwrap();
1083 assert_eq!(signal_state(pid), ProcessState::Exited);
1084 }
1085
1086 #[cfg(target_os = "linux")]
1088 #[test]
1089 fn the_signal_fallback_reports_an_unreaped_child_as_exited_on_linux() {
1090 use super::super::{ProcessHandle, ProcessState};
1091 let mut child = std::process::Command::new("true").spawn().unwrap();
1092 ProcessHandle::for_child(&child).unwrap().wait().unwrap();
1093 assert_eq!(signal_state(child.id()), ProcessState::Exited);
1094 child.wait().unwrap();
1095 }
1096
1097 #[cfg(feature = "launch")]
1098 #[test]
1099 fn a_new_session_child_is_watched_from_its_spawn() {
1100 let mut command = std::process::Command::new("true");
1101 let (mut child, mut exit) = spawn_in_new_session_watched(&mut command, None).unwrap();
1102 assert_eq!(exit.pid(), crate::ProcessId::of(&child));
1103 exit.wait().unwrap();
1104 assert!(child.wait().unwrap().success());
1105 }
1106
1107 #[test]
1108 fn a_failed_watch_stops_the_child_and_returns_the_error() {
1109 let mut command = std::process::Command::new("sleep");
1110 command.arg("30");
1111 let mut pid = None;
1112 let error = spawn_then(&mut command, |child| {
1113 pid = Some(crate::ProcessId::of(child));
1114 Err::<(), _>(io::Error::other("watch failed"))
1115 })
1116 .unwrap_err();
1117 assert_eq!(error.to_string(), "watch failed");
1118 assert_eq!(
1120 super::super::process_state(pid.unwrap()),
1121 super::super::ProcessState::Exited
1122 );
1123 }
1124
1125 #[test]
1126 fn a_new_session_child_leads_its_own_session_and_process_group() {
1127 unsafe extern "C" {
1128 fn getsid(pid: i32) -> i32;
1129 fn getpgid(pid: i32) -> i32;
1130 }
1131 let mut command = std::process::Command::new("sleep");
1132 command.arg("30");
1133 let mut child = spawn_in_new_session(&mut command).unwrap();
1134 let pid = child.id() as i32;
1135 let (child_sid, child_pgid, own_sid) = unsafe { (getsid(pid), getpgid(pid), getsid(0)) };
1138 child.kill().unwrap();
1139 child.wait().unwrap();
1140 assert_eq!(child_sid, pid, "not a session leader");
1141 assert_eq!(child_pgid, pid, "not a process group leader");
1142 assert_ne!(child_sid, own_sid, "still in the caller's session");
1143 }
1144
1145 #[test]
1146 fn a_process_this_user_may_not_signal_is_alive_not_exited() {
1147 use super::super::ProcessState;
1148 assert_eq!(liveness_from_errno(Some(1)), ProcessState::Alive);
1149 assert_eq!(liveness_from_errno(Some(3)), ProcessState::Exited);
1150 assert_eq!(liveness_from_errno(Some(22)), ProcessState::Unknown);
1151 assert_eq!(liveness_from_errno(None), ProcessState::Unknown);
1152 }
1153
1154 fn temp_socket(name: &str) -> std::path::PathBuf {
1155 let mut p = std::env::temp_dir();
1156 p.push(format!(
1157 "daemon-transport-test-{}-{}.sock",
1158 std::process::id(),
1159 name
1160 ));
1161 let _ = std::fs::remove_file(&p);
1162 p
1163 }
1164
1165 #[test]
1166 fn setup_write_deadline_bounds_a_peer_that_never_reads() {
1167 let path = temp_socket("write-deadline");
1168 let listener = std::os::unix::net::UnixListener::bind(&path).unwrap();
1169 let (release, wait) = std::sync::mpsc::channel();
1170 let server = std::thread::spawn(move || {
1171 let (_peer, _) = listener.accept().unwrap();
1172 let _ = wait.recv_timeout(Duration::from_secs(2));
1173 });
1174 let transport = UnixDuplex::connect(&path).unwrap();
1175 assert!(transport.credentials().unwrap().same_user);
1176 transport
1177 .set_read_deadline(Some(Duration::from_millis(100)))
1178 .unwrap();
1179 let (_read, mut write) = transport.split().unwrap();
1180 let error = write.write_all(&vec![0; 16 << 20]).unwrap_err();
1181 release.send(()).ok();
1182 server.join().unwrap();
1183 assert_eq!(error.kind(), io::ErrorKind::TimedOut);
1184 let _ = std::fs::remove_file(path);
1185 }
1186
1187 #[test]
1188 fn clearing_setup_restores_writes_after_the_original_deadline() {
1189 let (stream, mut peer) = UnixStream::pair().unwrap();
1190 let transport = UnixDuplex {
1191 stream,
1192 read_deadline: Cell::new(None),
1193 };
1194 assert!(transport.credentials().unwrap().same_user);
1195 assert!(fd_credentials(-1).is_err());
1196 transport
1197 .set_read_deadline(Some(Duration::from_millis(1)))
1198 .unwrap();
1199 let (mut read, mut write) = transport.split().unwrap();
1200 read.clear_read_deadline().unwrap();
1201 #[expect(
1202 clippy::disallowed_methods,
1203 reason = "The test must read after the cleared socket deadline has passed."
1204 )]
1205 std::thread::sleep(Duration::from_millis(5));
1206 write.write_all(b"long job").unwrap();
1207 let mut bytes = [0; 8];
1208 peer.read_exact(&mut bytes).unwrap();
1209 assert_eq!(&bytes, b"long job");
1210 }
1211
1212 #[test]
1213 fn an_absent_socket_times_out_rather_than_hanging() {
1214 let path = temp_socket("absent");
1215 let started = Instant::now();
1216 let err = UnixDuplex::connect_until(&path, Instant::now() + Duration::from_millis(150));
1218 assert_eq!(err.err(), Some(ConnectError::ConnectTimeout));
1219 assert!(
1220 started.elapsed() < Duration::from_secs(2),
1221 "connect did not respect its deadline"
1222 );
1223 }
1224
1225 #[test]
1226 fn an_overlong_endpoint_fails_at_once_instead_of_timing_out() {
1227 let path = std::path::PathBuf::from(format!(
1229 "/{}",
1230 "a".repeat(super::super::unix_socket::MAX_PATH_BYTES)
1231 ));
1232 let started = Instant::now();
1233 let err = UnixDuplex::connect_until(&path, Instant::now() + Duration::from_secs(5));
1234 assert_eq!(err.err(), Some(ConnectError::EndpointTooLong));
1235 assert!(
1236 started.elapsed() < Duration::from_millis(500),
1237 "it retried a path that cannot work"
1238 );
1239 }
1240
1241 #[test]
1242 fn a_listening_socket_connects_and_splits() {
1243 let path = temp_socket("listen");
1244 let listener = UnixListener::bind(&path).unwrap();
1245
1246 let accept = std::thread::spawn(move || listener.accept().map(|(s, _)| s));
1247
1248 let duplex = UnixDuplex::connect(&path).expect("connect");
1249 let (mut r, mut w) = duplex.split().expect("split");
1250
1251 let mut server = accept.join().unwrap().unwrap();
1252
1253 w.write_all(b"ping").unwrap();
1254 w.shutdown_write().unwrap();
1255
1256 let mut got = [0u8; 4];
1257 server.read_exact(&mut got).unwrap();
1258 assert_eq!(&got, b"ping");
1259
1260 server.write_all(b"pong").unwrap();
1262 drop(server);
1263
1264 let mut back = Vec::new();
1265 r.read_to_end(&mut back).unwrap();
1266 assert_eq!(back, b"pong");
1267
1268 let _ = std::fs::remove_file(&path);
1269 }
1270
1271 #[test]
1272 fn the_two_halves_are_independent_file_descriptors() {
1273 let path = temp_socket("split");
1276 let listener = UnixListener::bind(&path);
1277 let accept = std::thread::spawn(move || listener.unwrap().accept().map(|(s, _)| s));
1278
1279 let (mut r, mut w) = UnixDuplex::connect(&path).unwrap().split().unwrap();
1280 let mut server = accept.join().unwrap().unwrap();
1281
1282 let reader = std::thread::spawn(move || {
1283 let mut buf = [0u8; 4];
1284 r.read_exact(&mut buf).map(|_| buf)
1285 });
1286
1287 w.write_all(b"up__").unwrap();
1289 let mut got = [0u8; 4];
1290 server.read_exact(&mut got).unwrap();
1291 assert_eq!(&got, b"up__");
1292
1293 server.write_all(b"down").unwrap();
1294 assert_eq!(&reader.join().unwrap().unwrap(), b"down");
1295
1296 let _ = std::fs::remove_file(&path);
1297 }
1298
1299 #[test]
1300 fn a_deadline_bounds_a_peer_that_never_answers_the_preamble() {
1301 let path = temp_socket("silent");
1305 let listener = UnixListener::bind(&path).unwrap();
1306 let accept = std::thread::spawn(move || listener.accept().map(|(s, _)| s));
1307
1308 let duplex = UnixDuplex::connect(&path).expect("the kernel-level connect succeeds");
1309 duplex
1310 .set_read_deadline(Some(Duration::from_millis(100)))
1311 .expect("arm");
1312 let mut server = accept.join().unwrap().unwrap();
1313
1314 let (mut r, mut w) = duplex.split().expect("split");
1315 let started = Instant::now();
1316 let outcome = crate::local::test_handshake(&mut w, &mut r, "test");
1317 let elapsed = started.elapsed();
1318
1319 assert!(outcome.is_err(), "a silent peer must not pass");
1320 assert!(
1321 elapsed < Duration::from_secs(2),
1322 "handshake ignored its deadline: {elapsed:?}"
1323 );
1324 r.clear_read_deadline().unwrap();
1327 let mut drained = 0;
1333 while drained < 6 {
1334 match server.read(&mut [0u8; 64]) {
1335 Ok(0) => break,
1336 Ok(n) => drained += n,
1337 Err(e) => panic!("server drain failed: {e}"),
1338 }
1339 }
1340 drop(server);
1341 let mut sink = [0u8; 1];
1342 assert_eq!(r.read(&mut sink).unwrap(), 0);
1343
1344 let _ = std::fs::remove_file(&path);
1345 }
1346
1347 #[test]
1348 fn a_drip_fed_ack_cannot_extend_the_establishment_deadline() {
1349 let path = temp_socket("drip");
1350 let listener = UnixListener::bind(&path).unwrap();
1351 let server = std::thread::spawn(move || {
1352 let (mut connection, _) = listener.accept().unwrap();
1353 let mut preamble = [0u8; 6];
1354 connection.read_exact(&mut preamble).unwrap();
1355 for byte in *b"ready\n" {
1356 if connection.write_all(&[byte]).is_err() {
1357 break;
1358 }
1359 connection.flush().unwrap();
1360 #[expect(
1361 clippy::disallowed_methods,
1362 reason = "The fixture trickles bytes to verify the handshake uses one total deadline."
1363 )]
1364 std::thread::sleep(Duration::from_millis(30));
1365 }
1366 });
1367
1368 let duplex = UnixDuplex::connect(&path).unwrap();
1369 duplex
1370 .set_read_deadline(Some(Duration::from_millis(150)))
1371 .unwrap();
1372 let (mut read, mut write) = duplex.split().unwrap();
1373 let started = Instant::now();
1374 let outcome = crate::local::test_handshake(&mut write, &mut read, "test");
1375 let elapsed = started.elapsed();
1376
1377 assert!(outcome.is_err(), "a slow drip must not reset the deadline");
1378 assert!(
1379 elapsed < Duration::from_millis(350),
1380 "drip feed extended the absolute deadline: {elapsed:?}"
1381 );
1382 server.join().unwrap();
1383 let _ = std::fs::remove_file(&path);
1384 }
1385
1386 #[test]
1387 fn a_socket_with_no_permissions_is_reported_as_permission_denied() {
1388 use std::os::unix::fs::PermissionsExt;
1391
1392 let path = temp_socket("perms");
1393 let _listener = UnixListener::bind(&path).unwrap();
1394 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o000)).unwrap();
1395
1396 let started = Instant::now();
1397 let result = UnixDuplex::connect(&path);
1398 let elapsed = started.elapsed();
1399
1400 if let Err(e) = result {
1403 assert_eq!(e, ConnectError::PermissionDenied);
1404 assert!(
1405 elapsed < crate::local::CONNECT_BUDGET,
1406 "permission failure burned the retry budget instead of short-circuiting"
1407 );
1408 }
1409
1410 let _ = std::fs::remove_file(&path);
1411 }
1412
1413 #[test]
1414 fn a_live_endpoint_connect_once_does_not_retry_sleep() {
1415 use super::super::{Duplex, RETRY_INTERVAL};
1416
1417 let path = temp_socket("live-once");
1418 let listener = UnixListener::bind(&path).unwrap();
1419 let accept = std::thread::spawn(move || listener.accept());
1420 let started = std::time::Instant::now();
1421 let duplex = UnixDuplex::connect_once(&path).expect("live endpoint");
1422 let elapsed = started.elapsed();
1423 assert!(
1424 elapsed < RETRY_INTERVAL,
1425 "live connect inserted a retry sleep: {elapsed:?}"
1426 );
1427 drop(duplex);
1428 let _ = accept.join();
1429 let _ = std::fs::remove_file(&path);
1430 }
1431}
1432
1433#[cfg(all(test, not(target_os = "macos")))]
1434mod visible_pid_tests {
1435 use super::visible_pid;
1436
1437 #[test]
1438 fn pid_zero_and_negative_values_are_not_visible_pids() {
1439 assert_eq!(visible_pid(0), None);
1440 assert_eq!(visible_pid(-1), None);
1441 assert_eq!(visible_pid(42), Some(42));
1442 }
1443}