wasma-client 1.3.0-beta-release

Windows Assignment System Monitoring Architecture - Cross-platform resource-aware window management
// backend_selector.rs
// Runtime Backend Selection — X11 ve Wayland arasında dinamik geçiş

use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Backend {
    X11,
    Wayland,
}

impl std::fmt::Display for Backend {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Backend::X11 => write!(f, "X11"),
            Backend::Wayland => write!(f, "Wayland"),
        }
    }
}

/// Backend info — compositor binary tarafından yazılır, client tarafından okunur.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BackendInfo {
    pub backend: Backend,
    pub display: String,
    pub socket: Option<String>,   // Wayland socket path (e.g. "wasma-0" or "/run/user/1000/wasma-0")
    pub xdisplay: Option<String>, // X11 DISPLAY value (e.g. ":0")
}

impl BackendInfo {
    pub fn x11(display: &str) -> Self {
        Self {
            backend: Backend::X11,
            display: display.to_string(),
            socket: None,
            xdisplay: Some(display.to_string()),
        }
    }

    pub fn wayland(display: &str, socket: &str) -> Self {
        Self {
            backend: Backend::Wayland,
            display: display.to_string(),
            socket: Some(socket.to_string()),
            xdisplay: None,
        }
    }

    /// Compositor binary tarafından çağrılır — struct'taki bilgiyi diske yazar.
    /// Orijinal kodda read→write yapılıyordu (bug). Şimdi self serialize ediliyor.
    pub fn write_to_file(&self, path: &str) -> std::io::Result<()> {
        let json = serde_json::to_string(self)
            .map_err(std::io::Error::other)?;
        fs::write(path, json)
    }

    /// Client tarafından kullanılır.
    pub fn read_from_file(path: &str) -> std::io::Result<Self> {
        let contents = fs::read_to_string(path)?;
        serde_json::from_str(&contents)
            .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
    }

    /// Wayland socket'in gerçek path'ini döndürür.
    ///
    /// Öncelik sırası:
    ///   1. `socket` field'ı — eğer zaten absolute path ise direkt kullan
    ///   2. `socket` field'ı — relative ise XDG_RUNTIME_DIR ile birleştir
    ///   3. `display` field'ı — aynı mantıkla
    pub fn wayland_socket_path(&self) -> Option<String> {
        let runtime_dir = std::env::var("XDG_RUNTIME_DIR")
            .unwrap_or_else(|_| format!("/run/user/{}", unsafe { libc::getuid() }));

        let resolve = |s: &str| -> String {
            if s.starts_with('/') {
                s.to_string()
            } else {
                format!("{}/{}", runtime_dir, s)
            }
        };

        // Try socket field first, then display field
        self.socket
            .as_deref()
            .map(resolve)
            .or_else(|| {
                if self.backend == Backend::Wayland {
                    Some(resolve(&self.display))
                } else {
                    None
                }
            })
    }
}

pub struct BackendSelector;

impl BackendSelector {
    pub const BACKEND_INFO_PATH: &'static str = "/tmp/wasma-backend-info.json";

    /// Kullanıcıdan interaktif backend seçimi al.
    pub fn select_interactive() -> Result<Backend, String> {
        println!("\n=== Backend Seçimi ===");
        println!("[1] X11");
        println!("[2] Wayland");
        print!("Choose Backend (1-2): ");

        use std::io::{self, Write};
        io::stdout().flush().ok();

        let mut input = String::new();
        io::stdin()
            .read_line(&mut input)
            .map_err(|e| format!("Input error: {}", e))?;

        match input.trim() {
            "1" => Ok(Backend::X11),
            "2" => Ok(Backend::Wayland),
            _ => Err("Invalid selection. Please enter 1 or 2.".to_string()),
        }
    }

    /// Mevcut çalışan backend'i detect et.
    ///
    /// Öncelik: info dosyası → env var → varsayılan X11
    pub fn detect() -> Backend {
        if let Ok(info) = BackendInfo::read_from_file(Self::BACKEND_INFO_PATH) {
            return info.backend;
        }
        if std::env::var("WAYLAND_DISPLAY").is_ok() {
            return Backend::Wayland;
        }
        Backend::X11
    }

    /// Seçilen backend için gerekli env var'larını WASMA process'ine set et.
    /// Child process env'ini bu fonksiyon DEĞİL, `apply_backend_env` yönetir.
    pub fn setup(backend: Backend) -> Result<(), String> {
        match backend {
            Backend::Wayland => {
                // Compositor henüz başlamamışsa başlatmaya çalış
                if std::env::var("WAYLAND_DISPLAY").is_err()
                    && !Path::new(Self::BACKEND_INFO_PATH).exists()
                {
                    let _ = Self::try_spawn_wayland_backend();
                    std::thread::sleep(std::time::Duration::from_millis(400));
                }

                // Socket path'i bul ve doğrula
                let runtime = std::env::var("XDG_RUNTIME_DIR")
                    .unwrap_or_else(|_| "/run/user/1000".to_string());

                let socket = Self::find_valid_wayland_socket(&runtime);
                std::env::set_var("WAYLAND_DISPLAY", &socket);
                std::env::set_var("QT_QPA_PLATFORM", "wayland");
                std::env::set_var("GDK_BACKEND", "wayland");
                std::env::set_var("SDL_VIDEODRIVER", "wayland");
                tracing::info!("WAYLAND_DISPLAY set to: {}", socket);
            }
            Backend::X11 => {
                if std::env::var("DISPLAY").is_err() {
                    let _ = Self::try_spawn_x11_backend();
                    std::thread::sleep(std::time::Duration::from_millis(400));
                }

                if let Ok(info) = BackendInfo::read_from_file(Self::BACKEND_INFO_PATH) {
                    if let Some(display) = &info.xdisplay {
                        std::env::set_var("DISPLAY", display);
                    }
                    std::env::set_var("QT_QPA_PLATFORM", "xcb");
                    std::env::set_var("GDK_BACKEND", "x11");
                } else if let Ok(existing) = std::env::var("DISPLAY") {
                    tracing::info!("Using existing DISPLAY={}", existing);
                    std::env::set_var("QT_QPA_PLATFORM", "xcb");
                    std::env::set_var("GDK_BACKEND", "x11");
                } else {
                    // Son çare — varsayılan :0
                    std::env::set_var("DISPLAY", ":0");
                    std::env::set_var("QT_QPA_PLATFORM", "xcb");
                    std::env::set_var("GDK_BACKEND", "x11");
                    tracing::warn!("X11 fallback: DISPLAY=:0");
                }
            }
        }
        Ok(())
    }

    /// Gerçekte var olan Wayland socket'i bul.
    /// info dosyası → env var → XDG_RUNTIME_DIR scan → sistem fallback
    fn find_valid_wayland_socket(runtime: &str) -> String {
        let check = |s: &str| -> Option<String> {
            let path = if s.starts_with('/') {
                s.to_string()
            } else {
                format!("{}/{}", runtime, s)
            };
            if std::path::Path::new(&path).exists() {
                Some(path)
            } else {
                None
            }
        };

        // 1. info dosyası
        if let Ok(info) = BackendInfo::read_from_file(Self::BACKEND_INFO_PATH) {
            if let Some(ref s) = info.socket {
                if let Some(p) = check(s) { return p; }
            }
            if info.backend == Backend::Wayland {
                if let Some(p) = check(&info.display) { return p; }
            }
        }

        // 2. mevcut env var
        if let Ok(existing) = std::env::var("WAYLAND_DISPLAY") {
            if let Some(p) = check(&existing) {
                return p;
            }
        }

        // 3. XDG_RUNTIME_DIR içinde wasma-* socket ara
        if let Ok(entries) = std::fs::read_dir(runtime) {
            #[cfg(unix)]
            use std::os::unix::fs::FileTypeExt;
            for entry in entries.flatten() {
                let name = entry.file_name();
                let name_str = name.to_string_lossy();
                if name_str.starts_with("wasma") {
                    #[cfg(unix)]
                    {
                        if entry.file_type().map(|t| t.is_socket()).unwrap_or(false) {
                            return entry.path().to_string_lossy().into_owned();
                        }
                    }
                    #[cfg(not(unix))]
                    {
                        return entry.path().to_string_lossy().into_owned();
                    }
                }
            }
        }

        // 4. sistem Wayland socket'i
        for name in &["wayland-1", "wayland-0"] {
            let path = format!("{}/{}", runtime, name);
            if std::path::Path::new(&path).exists() {
                tracing::warn!("Falling back to system Wayland socket: {}", path);
                return path;
            }
        }

        // son çare
        format!("{}/wayland-0", runtime)
    }

    fn try_spawn_wayland_backend() -> Result<(), String> {
        let candidates = [
            "waylandbackend",
            "./target/release/waylandbackend",
            "/usr/local/bin/waylandbackend",
        ];
        for candidate in &candidates {
            if let Ok(child) = std::process::Command::new(candidate)
                .stdout(std::process::Stdio::null())
                .stderr(std::process::Stdio::null())
                .spawn()
            {
                tracing::info!(
                    "Started Wayland backend from {} (pid={})",
                    candidate,
                    child.id()
                );
                return Ok(());
            }
        }
        Err("Could not spawn waylandbackend; ensure it is built and in PATH".to_string())
    }

    fn try_spawn_x11_backend() -> Result<(), String> {
        let candidates = [
            "x11-backend",
            "./target/release/x11-backend",
            "/usr/local/bin/x11-backend",
        ];
        for candidate in &candidates {
            if let Ok(child) = std::process::Command::new(candidate)
                .stdout(std::process::Stdio::null())
                .stderr(std::process::Stdio::null())
                .spawn()
            {
                tracing::info!(
                    "Started X11 backend from {} (pid={})",
                    candidate,
                    child.id()
                );
                return Ok(());
            }
        }
        Err("Could not spawn x11-backend; ensure it is built and in PATH".to_string())
    }

    /// Mevcut sistemde hangi backend'ler kullanılabilir?
    pub fn probe_available() -> Vec<Backend> {
        let mut available = Vec::new();

        if std::env::var("WAYLAND_DISPLAY").is_ok()
            || Path::new(Self::BACKEND_INFO_PATH).exists()
        {
            available.push(Backend::Wayland);
        }

        if std::env::var("DISPLAY").is_ok() {
            available.push(Backend::X11);
        }

        available
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_backend_display() {
        assert_eq!(Backend::X11.to_string(), "X11");
        assert_eq!(Backend::Wayland.to_string(), "Wayland");
    }

    #[test]
    fn test_backend_info_x11() {
        let info = BackendInfo::x11(":0");
        assert_eq!(info.backend, Backend::X11);
        assert_eq!(info.display, ":0");
        assert_eq!(info.xdisplay, Some(":0".to_string()));
    }

    #[test]
    fn test_backend_info_wayland() {
        let info = BackendInfo::wayland("wasma-0", "wasma-0");
        assert_eq!(info.backend, Backend::Wayland);
        assert_eq!(info.socket, Some("wasma-0".to_string()));
    }

    #[test]
    fn test_write_to_file_roundtrip() {
        let path = "/tmp/wasma-test-backend-info.json";
        let info = BackendInfo::wayland("wasma-0", "wasma-0");
        info.write_to_file(path).expect("write failed");
        let read_back = BackendInfo::read_from_file(path).expect("read failed");
        assert_eq!(read_back.backend, Backend::Wayland);
        assert_eq!(read_back.socket, Some("wasma-0".to_string()));
        std::fs::remove_file(path).ok();
    }

    #[test]
    fn test_wayland_socket_path_absolute() {
        let info = BackendInfo {
            backend: Backend::Wayland,
            display: "wasma-0".to_string(),
            socket: Some("/run/user/1000/wasma-0".to_string()),
            xdisplay: None,
        };
        assert_eq!(
            info.wayland_socket_path(),
            Some("/run/user/1000/wasma-0".to_string())
        );
    }

    #[test]
    fn test_wayland_socket_path_relative() {
        std::env::set_var("XDG_RUNTIME_DIR", "/run/user/9999");
        let info = BackendInfo {
            backend: Backend::Wayland,
            display: "wasma-0".to_string(),
            socket: Some("wasma-0".to_string()),
            xdisplay: None,
        };
        assert_eq!(
            info.wayland_socket_path(),
            Some("/run/user/9999/wasma-0".to_string())
        );
    }
}