1pub use wallr_common::ipc::*;
5
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use std::time::Duration;
9use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
10use tokio::net::{UnixListener, UnixStream};
11use tokio::sync::Semaphore;
12
13pub const MAX_IPC_RESPONSE_BYTES: usize = 1024 * 1024;
16
17const CONNECT_TIMEOUT: Duration = Duration::from_secs(5);
18const WRITE_TIMEOUT: Duration = Duration::from_secs(5);
19const REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
20const RESPONSE_TIMEOUT: Duration = Duration::from_secs(120);
21const HANDLER_TIMEOUT: Duration = Duration::from_secs(120);
22const MAX_CONCURRENT_CONNECTIONS: usize = 64;
23
24#[derive(Debug, thiserror::Error)]
25pub enum IpcError {
26 #[error("daemon not running: {0}")]
27 DaemonNotRunning(String),
28 #[error("daemon already running: {0}")]
29 AlreadyRunning(String),
30 #[error(
31 "refusing to use IPC socket path {0}: it exists but is not a socket owned by this user"
32 )]
33 UnsafeSocketPath(String),
34 #[error("IPC request/response timed out: {0}")]
35 Timeout(String),
36 #[error("IPC response exceeds {MAX_IPC_RESPONSE_BYTES} bytes")]
37 ResponseTooLarge,
38 #[error("IPC I/O error: {0}")]
39 Io(#[from] std::io::Error),
40 #[error("protocol serialization/deserialization error: {0}")]
41 Protocol(#[from] serde_json::Error),
42}
43
44pub struct IpcSocket {
51 path: PathBuf,
52 listener: UnixListener,
53 dev: u64,
54 ino: u64,
55 _lock: std::fs::File,
56}
57
58impl IpcSocket {
59 pub async fn acquire<P: AsRef<Path>>(path: P) -> Result<Self, IpcError> {
65 let path = path.as_ref().to_path_buf();
66 let lock_path = socket_lock_path(&path);
67 let lock = open_lock_file(&lock_path)?;
68 lock_exclusive(&lock).map_err(|err| {
69 if err.kind() == std::io::ErrorKind::WouldBlock {
70 IpcError::AlreadyRunning(path.to_string_lossy().to_string())
71 } else {
72 IpcError::Io(err)
73 }
74 })?;
75
76 match std::fs::symlink_metadata(&path) {
77 Ok(metadata) => {
78 use std::os::unix::fs::{FileTypeExt, MetadataExt};
79
80 if !metadata.file_type().is_socket() {
81 return Err(IpcError::UnsafeSocketPath(
82 path.to_string_lossy().to_string(),
83 ));
84 }
85 let euid = unsafe { libc::geteuid() };
88 if metadata.uid() != euid {
89 return Err(IpcError::UnsafeSocketPath(
90 path.to_string_lossy().to_string(),
91 ));
92 }
93 match UnixStream::connect(&path).await {
94 Ok(_) => {
95 return Err(IpcError::AlreadyRunning(path.to_string_lossy().to_string()));
96 }
97 Err(err)
98 if matches!(
99 err.kind(),
100 std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::NotFound
101 ) =>
102 {
103 std::fs::remove_file(&path)?;
104 }
105 Err(err) => {
106 return Err(IpcError::Io(err));
109 }
110 }
111 }
112 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
113 Err(err) => return Err(IpcError::Io(err)),
114 }
115
116 let listener = UnixListener::bind(&path)?;
117 use std::os::unix::fs::{MetadataExt, PermissionsExt};
118 if let Err(err) = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)) {
119 let _ = std::fs::remove_file(&path);
120 return Err(IpcError::Io(err));
121 }
122 let metadata = match std::fs::symlink_metadata(&path) {
123 Ok(metadata) => metadata,
124 Err(err) => {
125 let _ = std::fs::remove_file(&path);
126 return Err(IpcError::Io(err));
127 }
128 };
129
130 Ok(Self {
131 path,
132 listener,
133 dev: metadata.dev(),
134 ino: metadata.ino(),
135 _lock: lock,
136 })
137 }
138
139 pub fn path(&self) -> &Path {
140 &self.path
141 }
142
143 pub async fn serve<F, Fut>(self, handler: F) -> Result<(), IpcError>
149 where
150 F: Fn(IpcCommand) -> Fut + Send + Sync + 'static,
151 Fut: std::future::Future<Output = IpcResponse> + Send + 'static,
152 {
153 let handler = Arc::new(handler);
154 let permits = Arc::new(Semaphore::new(MAX_CONCURRENT_CONNECTIONS));
155 loop {
156 let permit = match Arc::clone(&permits).acquire_owned().await {
157 Ok(permit) => permit,
158 Err(_) => return Ok(()),
159 };
160 match self.listener.accept().await {
161 Ok((stream, _)) => {
162 let handler = Arc::clone(&handler);
163 tokio::spawn(async move {
164 let _permit = permit;
165 handle_connection(stream, handler).await;
166 });
167 }
168 Err(err) => {
169 tracing::error!("IPC accept error: {:?}", err);
170 tokio::time::sleep(Duration::from_millis(100)).await;
171 }
172 }
173 }
174 }
175
176 pub fn spawn<F, Fut>(self, handler: F)
178 where
179 F: Fn(IpcCommand) -> Fut + Send + Sync + 'static,
180 Fut: std::future::Future<Output = IpcResponse> + Send + 'static,
181 {
182 tokio::spawn(async move {
183 if let Err(err) = self.serve(handler).await {
184 tracing::error!("IPC server stopped: {err}");
185 }
186 });
187 }
188}
189
190#[derive(Debug, Clone, Copy)]
193pub struct IpcSocketIdentity {
194 dev: u64,
195 ino: u64,
196}
197
198impl IpcSocket {
199 pub fn identity(&self) -> IpcSocketIdentity {
200 IpcSocketIdentity {
201 dev: self.dev,
202 ino: self.ino,
203 }
204 }
205}
206
207pub fn remove_socket_if_owned(path: &Path, identity: IpcSocketIdentity) -> bool {
210 let owned = std::fs::symlink_metadata(path)
211 .is_ok_and(|metadata| existed_as_same_socket(&metadata, identity.dev, identity.ino));
212 if owned {
213 let _ = std::fs::remove_file(path);
214 }
215 owned
216}
217
218impl Drop for IpcSocket {
219 fn drop(&mut self) {
220 let _ = remove_socket_if_owned(&self.path, self.identity());
221 }
222}
223
224fn existed_as_same_socket(metadata: &std::fs::Metadata, dev: u64, ino: u64) -> bool {
225 use std::os::unix::fs::{FileTypeExt, MetadataExt};
226 metadata.file_type().is_socket() && metadata.dev() == dev && metadata.ino() == ino
227}
228
229fn socket_lock_path(path: &Path) -> PathBuf {
230 let mut name = path
231 .file_name()
232 .map(|name| name.to_os_string())
233 .unwrap_or_else(|| std::ffi::OsString::from("wallr.sock"));
234 name.push(".lock");
235 path.with_file_name(name)
236}
237
238fn open_lock_file(path: &Path) -> std::io::Result<std::fs::File> {
239 use std::os::unix::fs::OpenOptionsExt;
240 std::fs::OpenOptions::new()
241 .read(true)
242 .write(true)
243 .create(true)
244 .truncate(false)
245 .mode(0o600)
246 .open(path)
247}
248
249fn lock_exclusive(file: &std::fs::File) -> std::io::Result<()> {
250 use std::os::unix::io::AsRawFd;
251 let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
254 if result == 0 {
255 Ok(())
256 } else {
257 Err(std::io::Error::last_os_error())
258 }
259}
260
261async fn handle_connection<F, Fut>(stream: UnixStream, handler: Arc<F>)
262where
263 F: Fn(IpcCommand) -> Fut + Send + Sync + 'static,
264 Fut: std::future::Future<Output = IpcResponse> + Send + 'static,
265{
266 let (reader, mut writer) = tokio::io::split(stream);
267 let reader = BufReader::new(reader);
268 let mut limited = reader.take((MAX_IPC_BYTES + 1) as u64);
269 let mut buf = Vec::with_capacity(1024);
270 let response =
271 match tokio::time::timeout(REQUEST_TIMEOUT, limited.read_until(b'\n', &mut buf)).await {
272 Err(_) => IpcResponse::err("IPC request timed out"),
273 Ok(Ok(0)) => return,
274 Ok(Ok(_)) if buf.len() > MAX_IPC_BYTES => {
275 IpcResponse::err(format!("request exceeds {MAX_IPC_BYTES} bytes"))
276 }
277 Ok(Ok(_)) => match serde_json::from_slice::<IpcCommand>(&buf) {
278 Ok(cmd) => match validate_command(&cmd) {
279 Ok(()) => match tokio::time::timeout(HANDLER_TIMEOUT, handler(cmd)).await {
280 Ok(response) => response,
281 Err(_) => IpcResponse::err("IPC command timed out"),
282 },
283 Err(reason) => IpcResponse::err(reason),
284 },
285 Err(e) => IpcResponse::err(format!("invalid IPC command: {e}")),
286 },
287 Ok(Err(e)) => IpcResponse::err(format!("IPC read error: {e}")),
288 };
289 write_response(&mut writer, &response).await;
290}
291
292async fn write_response(writer: &mut tokio::io::WriteHalf<UnixStream>, response: &IpcResponse) {
293 let Ok(res_data) = serde_json::to_vec(response) else {
294 return;
295 };
296 let write = async {
297 writer.write_all(&res_data).await?;
298 writer.write_all(b"\n").await?;
299 writer.flush().await
300 };
301 let _ = tokio::time::timeout(WRITE_TIMEOUT, write).await;
302}
303
304async fn read_response(reader: &mut BufReader<UnixStream>) -> Result<Vec<u8>, IpcError> {
305 let mut limited = reader.take((MAX_IPC_RESPONSE_BYTES + 1) as u64);
306 let mut response_line = Vec::new();
307 let read = limited.read_until(b'\n', &mut response_line);
308 match tokio::time::timeout(RESPONSE_TIMEOUT, read).await {
309 Ok(Ok(0)) => Err(IpcError::Protocol(serde_json::Error::io(
310 std::io::Error::new(
311 std::io::ErrorKind::UnexpectedEof,
312 "daemon closed the connection without a response",
313 ),
314 ))),
315 Ok(Ok(_)) if response_line.len() > MAX_IPC_RESPONSE_BYTES => {
316 Err(IpcError::ResponseTooLarge)
317 }
318 Ok(Ok(_)) => Ok(response_line),
319 Ok(Err(err)) => Err(IpcError::Io(err)),
320 Err(_) => Err(IpcError::Timeout("waiting for daemon response".to_string())),
321 }
322}
323
324pub async fn send_ipc_command<P: AsRef<Path>>(
325 socket_path: P,
326 command: IpcCommand,
327) -> Result<IpcResponse, IpcError> {
328 let socket_path = socket_path.as_ref();
329 let mut stream =
330 match tokio::time::timeout(CONNECT_TIMEOUT, UnixStream::connect(socket_path)).await {
331 Ok(Ok(stream)) => stream,
332 Ok(Err(err)) => return Err(IpcError::DaemonNotRunning(err.to_string())),
333 Err(_) => {
334 return Err(IpcError::Timeout(format!(
335 "connecting to {}",
336 socket_path.display()
337 )));
338 }
339 };
340
341 let req_data = serde_json::to_vec(&command)?;
342 let write = async {
343 stream.write_all(&req_data).await?;
344 stream.write_all(b"\n").await?;
345 stream.flush().await
346 };
347 match tokio::time::timeout(WRITE_TIMEOUT, write).await {
348 Ok(Ok(())) => {}
349 Ok(Err(err)) => return Err(IpcError::Io(err)),
350 Err(_) => return Err(IpcError::Timeout("sending IPC request".to_string())),
351 }
352
353 let mut reader = BufReader::new(stream);
354 let response_line = read_response(&mut reader).await?;
355 let response: IpcResponse = serde_json::from_slice(&response_line)?;
356 Ok(response)
357}
358
359pub async fn start_ipc_server<P, F, Fut>(socket_path: P, handler: F) -> Result<(), IpcError>
362where
363 P: AsRef<Path>,
364 F: Fn(IpcCommand) -> Fut + Send + Sync + 'static,
365 Fut: std::future::Future<Output = IpcResponse> + Send + 'static,
366{
367 IpcSocket::acquire(socket_path).await?.spawn(handler);
368 Ok(())
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use std::os::unix::fs::PermissionsExt;
375
376 #[tokio::test]
377 async fn socket_is_private_and_removed_on_drop() {
378 let temp = tempfile::tempdir().unwrap();
379 let path = temp.path().join("wallr.sock");
380 let socket = IpcSocket::acquire(&path).await.unwrap();
381 let mode = std::fs::metadata(&path).unwrap().permissions().mode() & 0o777;
382 assert_eq!(mode, 0o600);
383 drop(socket);
384 assert!(!path.exists());
385 }
386
387 #[tokio::test]
388 async fn live_socket_blocks_a_second_daemon() {
389 let temp = tempfile::tempdir().unwrap();
390 let path = temp.path().join("wallr.sock");
391 let _first = IpcSocket::acquire(&path).await.unwrap();
392 assert!(matches!(
393 IpcSocket::acquire(&path).await,
394 Err(IpcError::AlreadyRunning(_))
395 ));
396 }
397
398 #[tokio::test]
399 async fn stale_socket_is_replaced_but_regular_files_are_preserved() {
400 let temp = tempfile::tempdir().unwrap();
401 let path = temp.path().join("wallr.sock");
402 drop(std::os::unix::net::UnixListener::bind(&path).unwrap());
403 assert!(path.exists());
404 let socket = IpcSocket::acquire(&path).await.unwrap();
405 drop(socket);
406 assert!(!path.exists());
407
408 let regular = temp.path().join("not-a-socket");
409 std::fs::write(®ular, b"user data").unwrap();
410 assert!(matches!(
411 IpcSocket::acquire(®ular).await,
412 Err(IpcError::UnsafeSocketPath(_))
413 ));
414 assert_eq!(std::fs::read(®ular).unwrap(), b"user data");
415 }
416
417 #[tokio::test]
418 async fn server_round_trips_commands_and_rejects_empty_preview_paths() {
419 let temp = tempfile::tempdir().unwrap();
420 let path = temp.path().join("wallr.sock");
421 let socket = IpcSocket::acquire(&path).await.unwrap();
422 let server_path = path.clone();
423 socket.spawn(|cmd| async move {
424 match cmd {
425 IpcCommand::Status => IpcResponse {
426 success: true,
427 message: Some("ok".into()),
428 },
429 other => IpcResponse::err(format!("unexpected {other:?}")),
430 }
431 });
432 let response = send_ipc_command(&server_path, IpcCommand::Status)
433 .await
434 .unwrap();
435 assert!(response.success);
436 assert_eq!(response.message.as_deref(), Some("ok"));
437
438 let response = send_ipc_command(
439 &server_path,
440 IpcCommand::Preview {
441 path: String::new(),
442 effect: None,
443 duration_ms: None,
444 fps: None,
445 no_theme: true,
446 theme_override: None,
447 monitor: None,
448 scaling_mode: None,
449 },
450 )
451 .await
452 .unwrap();
453 assert!(!response.success);
454 }
455}