@@ -305,6 +305,14 @@ impl Daemon { |
| 305 | 305 | let mut last_gar_reconnect = std::time::Instant::now(); |
| 306 | 306 | let gar_max_backoff = Duration::from_secs(60); |
| 307 | 307 | |
| 308 | + // X11 health check and reconnection state |
| 309 | + let x11_health_interval = Duration::from_secs(5); |
| 310 | + let mut last_x11_health_check = std::time::Instant::now(); |
| 311 | + let mut x11_reconnect_backoff = Duration::from_secs(1); |
| 312 | + let mut last_x11_reconnect = std::time::Instant::now(); |
| 313 | + let x11_max_backoff = Duration::from_secs(30); |
| 314 | + let mut x11_needs_reconnect = false; |
| 315 | + |
| 308 | 316 | // Track next slideshow time |
| 309 | 317 | let mut next_slideshow: Option<tokio::time::Instant> = self.state.slideshow_interval |
| 310 | 318 | .map(|d| tokio::time::Instant::now() + d); |
@@ -328,7 +336,31 @@ impl Daemon { |
| 328 | 336 | // Main event loop |
| 329 | 337 | // Note: Session lifecycle is handled by systemd (PartOf=graphical-session.target) |
| 330 | 338 | loop { |
| 331 | | - // Compute reconnection delay (if needed) before select |
| 339 | + // X11 health check (periodic, only if connected) |
| 340 | + if !x11_needs_reconnect && last_x11_health_check.elapsed() >= x11_health_interval { |
| 341 | + last_x11_health_check = std::time::Instant::now(); |
| 342 | + if !self.x11_is_alive() { |
| 343 | + tracing::warn!("X11 connection lost, will attempt reconnect"); |
| 344 | + self.conn = None; |
| 345 | + self.animation = None; // Animation renderer is now invalid |
| 346 | + x11_needs_reconnect = true; |
| 347 | + last_x11_reconnect = std::time::Instant::now(); |
| 348 | + } |
| 349 | + } |
| 350 | + |
| 351 | + // Compute X11 reconnection delay |
| 352 | + let x11_reconnect_delay = if x11_needs_reconnect { |
| 353 | + let elapsed = last_x11_reconnect.elapsed(); |
| 354 | + if elapsed < x11_reconnect_backoff { |
| 355 | + Some(x11_reconnect_backoff - elapsed) |
| 356 | + } else { |
| 357 | + Some(Duration::ZERO) |
| 358 | + } |
| 359 | + } else { |
| 360 | + None |
| 361 | + }; |
| 362 | + |
| 363 | + // Compute gar reconnection delay (if needed) before select |
| 332 | 364 | let reconnect_delay = if gar_needs_reconnect { |
| 333 | 365 | let elapsed = last_gar_reconnect.elapsed(); |
| 334 | 366 | if elapsed < gar_reconnect_backoff { |
@@ -451,6 +483,33 @@ impl Daemon { |
| 451 | 483 | } |
| 452 | 484 | } |
| 453 | 485 | |
| 486 | + // X11 reconnection timer (only when disconnected) |
| 487 | + _ = async { |
| 488 | + if let Some(delay) = x11_reconnect_delay { |
| 489 | + tokio::time::sleep(delay).await; |
| 490 | + } else { |
| 491 | + std::future::pending::<()>().await; |
| 492 | + } |
| 493 | + } => { |
| 494 | + tracing::debug!("Attempting to reconnect to X11..."); |
| 495 | + last_x11_reconnect = std::time::Instant::now(); |
| 496 | + |
| 497 | + if self.try_connect_x11() { |
| 498 | + x11_needs_reconnect = false; |
| 499 | + x11_reconnect_backoff = Duration::from_secs(1); |
| 500 | + tracing::info!("Reconnected to X11"); |
| 501 | + |
| 502 | + // Re-apply wallpaper after reconnection |
| 503 | + if let Err(e) = self.reapply_wallpaper() { |
| 504 | + tracing::warn!("Failed to re-apply wallpaper after X11 reconnect: {}", e); |
| 505 | + } |
| 506 | + } else { |
| 507 | + // Exponential backoff, max 30 seconds |
| 508 | + x11_reconnect_backoff = (x11_reconnect_backoff * 2).min(x11_max_backoff); |
| 509 | + tracing::debug!("X11 reconnection failed, next attempt in {:?}", x11_reconnect_backoff); |
| 510 | + } |
| 511 | + } |
| 512 | + |
| 454 | 513 | // gar reconnection timer (only when disconnected) |
| 455 | 514 | _ = async { |
| 456 | 515 | if let Some(delay) = reconnect_delay { |