Skip to main content

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::{Path, PathBuf},
13};
14
15use rustix::fs::{FlockOperation, flock};
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 associated 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    /// Returns the path of the listening socket.
161    pub fn socket_path(&self) -> &Path {
162        &self.socket_path
163    }
164}
165
166impl AsRawFd for ListeningSocket {
167    /// Returns a file descriptor that may be polled for readiness.
168    ///
169    /// This file descriptor may be polled using apis such as epoll and kqueue to be told when a client has
170    /// found the socket and is trying to connect.
171    ///
172    /// When the polling system reports the file descriptor is ready, you can use [`accept()`][Self::accept()]
173    /// to get a stream to the new client.
174    fn as_raw_fd(&self) -> RawFd {
175        self.listener.as_raw_fd()
176    }
177}
178
179impl AsFd for ListeningSocket {
180    /// Returns a file descriptor that may be polled for readiness.
181    ///
182    /// This file descriptor may be polled using apis such as epoll and kqueue to be told when a client has
183    /// found the socket and is trying to connect.
184    ///
185    /// When the polling system reports the file descriptor is ready, you can use [`accept()`][Self::accept()]
186    /// to get a stream to the new client.
187    fn as_fd(&self) -> BorrowedFd<'_> {
188        self.listener.as_fd()
189    }
190}
191
192impl Drop for ListeningSocket {
193    fn drop(&mut self) {
194        let _ = fs::remove_file(&self.socket_path);
195        let _ = fs::remove_file(&self.lock_path);
196    }
197}
198
199/// Error that can occur when trying to bind a [`ListeningSocket`]
200#[derive(Debug)]
201pub enum BindError {
202    /// The Environment variable `XDG_RUNTIME_DIR` is not set
203    RuntimeDirNotSet,
204    /// The application was not able to create a file in `XDG_RUNTIME_DIR`
205    PermissionDenied,
206    /// The requested socket name is already in use
207    AlreadyInUse,
208    /// Some other IO error occured
209    Io(io::Error),
210}
211
212impl std::error::Error for BindError {
213    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
214        match self {
215            BindError::RuntimeDirNotSet => None,
216            BindError::PermissionDenied => None,
217            BindError::AlreadyInUse => None,
218            BindError::Io(source) => Some(source),
219        }
220    }
221}
222
223impl std::fmt::Display for BindError {
224    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
225        match self {
226            BindError::RuntimeDirNotSet => {
227                write!(f, "Environment variable XDG_RUNTIME_DIR is not set or invalid")
228            }
229            BindError::PermissionDenied => write!(f, "Could not write to XDG_RUNTIME_DIR"),
230            BindError::AlreadyInUse => write!(f, "Requested socket name is already in use"),
231            BindError::Io(source) => write!(f, "I/O error: {source}"),
232        }
233    }
234}