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}