wayland_server/
socket.rs

1use std::{
2    env,
3    ffi::{OsStr, OsString},
4    fs::{self, File},
5    io,
6    os::unix::{
7        fs::OpenOptionsExt,
8        io::{AsFd, AsRawFd, BorrowedFd, RawFd},
9        net::{UnixListener, UnixStream},
10        prelude::MetadataExt,
11    },
12    path::PathBuf,
13};
14
15use rustix::fs::{flock, FlockOperation};
16
17/// An utility representing a unix socket on which your compositor is listening for new clients
18#[derive(Debug)]
19pub struct ListeningSocket {
20    listener: UnixListener,
21    _lock: File,
22    socket_path: PathBuf,
23    lock_path: PathBuf,
24    socket_name: Option<OsString>,
25}
26
27impl ListeningSocket {
28    /// Attempt to bind a listening socket with given name
29    ///
30    /// This method will acquire an associate lockfile. The socket will be created in the
31    /// directory pointed to by the `XDG_RUNTIME_DIR` environment variable.
32    pub fn bind<S: AsRef<OsStr>>(socket_name: S) -> Result<Self, BindError> {
33        let runtime_dir: PathBuf =
34            env::var("XDG_RUNTIME_DIR").map_err(|_| BindError::RuntimeDirNotSet)?.into();
35        if !runtime_dir.is_absolute() {
36            return Err(BindError::RuntimeDirNotSet);
37        }
38        let socket_path = runtime_dir.join(socket_name.as_ref());
39        let mut socket = Self::bind_absolute(socket_path)?;
40        socket.socket_name = Some(socket_name.as_ref().into());
41        Ok(socket)
42    }
43
44    /// Attempt to bind a listening socket from a sequence of names
45    ///
46    /// This method will repeatedly try to bind sockets in the form `{basename}-{n}` for values of `n`
47    /// yielded from the provided range and returns the first one that succeeds.
48    ///
49    /// This method will acquire an associate lockfile. The socket will be created in the
50    /// directory pointed to by the `XDG_RUNTIME_DIR` environment variable.
51    pub fn bind_auto(
52        basename: &str,
53        range: impl IntoIterator<Item = usize>,
54    ) -> Result<Self, BindError> {
55        for i in range {
56            // early return on any error except AlreadyInUse
57            match Self::bind(format!("{}-{}", basename, i)) {
58                Ok(socket) => return Ok(socket),
59                Err(BindError::RuntimeDirNotSet) => return Err(BindError::RuntimeDirNotSet),
60                Err(BindError::PermissionDenied) => return Err(BindError::PermissionDenied),
61                Err(BindError::Io(e)) => return Err(BindError::Io(e)),
62                Err(BindError::AlreadyInUse) => {}
63            }
64        }
65        Err(BindError::AlreadyInUse)
66    }
67
68    /// Attempt to bind a listening socket with given name
69    ///
70    /// The socket will be created at the specified path, and this method will acquire an associatet lockfile
71    /// alongside it.
72    pub fn bind_absolute(socket_path: PathBuf) -> Result<Self, BindError> {
73        let lock_path = socket_path.with_extension("lock");
74        let mut _lock;
75
76        // The locking code uses a loop to avoid an open()-flock() race condition, described in more
77        // detail in the comment below. The implementation roughtly follows the one from libbsd:
78        //
79        // https://gitlab.freedesktop.org/libbsd/libbsd/-/blob/73b25a8f871b3a20f6ff76679358540f95d7dbfd/src/flopen.c#L71
80        loop {
81            // open the lockfile
82            _lock = File::options()
83                .create(true)
84                .truncate(true)
85                .read(true)
86                .write(true)
87                .mode(0o660)
88                .open(&lock_path)
89                .map_err(|_| BindError::PermissionDenied)?;
90
91            // lock the lockfile
92            if flock(&_lock, FlockOperation::NonBlockingLockExclusive).is_err() {
93                return Err(BindError::AlreadyInUse);
94            }
95
96            // Verify that the file we locked is the same as the file on disk. An unlucky unlink()
97            // from a different thread which happens right between our open() and flock() may
98            // result in us successfully locking a now-nonexistent file, with another thread locking
99            // the same-named but newly created lock file, then both threads will think they have
100            // exclusive access to the same socket. To prevent this, check that we locked the actual
101            // currently existing file.
102            let fd_meta = _lock.metadata().map_err(BindError::Io)?;
103            let on_disk_meta = match fs::metadata(&lock_path) {
104                Ok(meta) => meta,
105                Err(err) if err.kind() == io::ErrorKind::NotFound => {
106                    // This can happen during the aforementioned race condition.
107                    continue;
108                }
109                Err(err) => return Err(BindError::Io(err)),
110            };
111
112            if fd_meta.dev() == on_disk_meta.dev() && fd_meta.ino() == on_disk_meta.ino() {
113                break;
114            }
115        }
116
117        // check if an old socket exists, and cleanup if relevant
118        match socket_path.try_exists() {
119            Ok(false) => {
120                // none exist, good
121            }
122            Ok(true) => {
123                // one exist, remove it
124                fs::remove_file(&socket_path).map_err(|_| BindError::AlreadyInUse)?;
125            }
126            Err(e) => {
127                // some error stat-ing the socket?
128                return Err(BindError::Io(e));
129            }
130        }
131
132        // At this point everything is good to start listening on the socket
133        let listener = UnixListener::bind(&socket_path).map_err(BindError::Io)?;
134
135        listener.set_nonblocking(true).map_err(BindError::Io)?;
136
137        Ok(Self { listener, _lock, socket_path, lock_path, socket_name: None })
138    }
139
140    /// Try to accept a new connection to the listening socket
141    ///
142    /// This method will never block, and return `Ok(None)` if no new connection is available.
143    #[must_use = "the client must be initialized by the display using `Display::insert_client` or else the client will hang forever"]
144    pub fn accept(&self) -> io::Result<Option<UnixStream>> {
145        match self.listener.accept() {
146            Ok((stream, _)) => Ok(Some(stream)),
147            Err(e) if e.kind() == io::ErrorKind::WouldBlock => Ok(None),
148            Err(e) => Err(e),
149        }
150    }
151
152    /// Returns the name of the listening socket.
153    ///
154    /// Will only be [`Some`] if that socket was created with [`bind()`][Self::bind()] or
155    /// [`bind_auto()`][Self::bind_auto()].
156    pub fn socket_name(&self) -> Option<&OsStr> {
157        self.socket_name.as_deref()
158    }
159}
160
161impl AsRawFd for ListeningSocket {
162    /// Returns a file descriptor that may be polled for readiness.
163    ///
164    /// This file descriptor may be polled using apis such as epoll and kqueue to be told when a client has
165    /// found the socket and is trying to connect.
166    ///
167    /// When the polling system reports the file descriptor is ready, you can use [`accept()`][Self::accept()]
168    /// to get a stream to the new client.
169    fn as_raw_fd(&self) -> RawFd {
170        self.listener.as_raw_fd()
171    }
172}
173
174impl AsFd for ListeningSocket {
175    /// Returns a file descriptor that may be polled for readiness.
176    ///
177    /// This file descriptor may be polled using apis such as epoll and kqueue to be told when a client has
178    /// found the socket and is trying to connect.
179    ///
180    /// When the polling system reports the file descriptor is ready, you can use [`accept()`][Self::accept()]
181    /// to get a stream to the new client.
182    fn as_fd(&self) -> BorrowedFd<'_> {
183        self.listener.as_fd()
184    }
185}
186
187impl Drop for ListeningSocket {
188    fn drop(&mut self) {
189        let _ = fs::remove_file(&self.socket_path);
190        let _ = fs::remove_file(&self.lock_path);
191    }
192}
193
194/// Error that can occur when trying to bind a [`ListeningSocket`]
195#[derive(Debug)]
196pub enum BindError {
197    /// The Environment variable `XDG_RUNTIME_DIR` is not set
198    RuntimeDirNotSet,
199    /// The application was not able to create a file in `XDG_RUNTIME_DIR`
200    PermissionDenied,
201    /// The requested socket name is already in use
202    AlreadyInUse,
203    /// Some other IO error occured
204    Io(io::Error),
205}
206
207impl std::error::Error for BindError {
208    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
209        match self {
210            BindError::RuntimeDirNotSet => None,
211            BindError::PermissionDenied => None,
212            BindError::AlreadyInUse => None,
213            BindError::Io(source) => Some(source),
214        }
215    }
216}
217
218impl std::fmt::Display for BindError {
219    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
220        match self {
221            BindError::RuntimeDirNotSet => {
222                write!(f, "Environment variable XDG_RUNTIME_DIR is not set or invalid")
223            }
224            BindError::PermissionDenied => write!(f, "Could not write to XDG_RUNTIME_DIR"),
225            BindError::AlreadyInUse => write!(f, "Requested socket name is already in use"),
226            BindError::Io(source) => write!(f, "I/O error: {source}"),
227        }
228    }
229}