//! Provides utilites for integration tests that render to a headless X11 server.
//!
//! ## X11 `DISPLAY` value allocation
//!
//! When starting headless X servers, some care needs to be taken in selecting
//! display numbers. An X server needs to have a unique display number and will
//! not start if it is assigned a display number that is already in use.
//!
//! Local X servers usually leave a lockfile in `/tmp`, like `/tmp/.X0-lock` for
//! display 0. Avoiding the use of display numbers that have a corresponding
//! lockfile will get you part of the way towards avoiding collisions, but
//! further coordination is provided by the [`Lock`] and
//! [`DisplayNumberRegistry`] structs. These interfaces provide a mechanism for
//! claiming a display number in a way that coordinates across processes and
//! should be more efficient than simply scanning `/tmp` each time a new display
//! number is needed.

use std::{
    fs,
    io,
    path::PathBuf,
    sync::atomic::{self, AtomicU16},
};

pub mod xvfb;
pub mod xwd;

/// Represents a lock on a display number, which is released on drop.
pub struct Lock {
    display: u16,
    path: PathBuf,
}

/// Errors that may occur when trying to lock a display number.
pub enum LockError {
    Io(io::Error),
    Locked,
}

impl Lock {
    pub(crate) fn new(display: u16, path: PathBuf) -> Self {
        Lock {
            display,
            path
        }
    }

    /// Returns the locked `DISPLAY` value.
    pub fn display(&self) -> u16 {
        self.display
    }
}

impl Drop for Lock {
    fn drop(&mut self) {
        // `file` should be unlinked already, but we explicitly try to delete it
        // and unwrap the result so that errors aren't dropped silently.
        match fs::remove_file(&self.path) {
            Ok(_) => (),
            Err(e) if e.kind() == io::ErrorKind::NotFound => (),
            Err(e) => panic!("could not unlink lock file: {:?}", e),
        }
    }
}

// Shared across threads in this process to help keep us from repeatedly asking
// for the same DISPLAY value.
static NEXT_DISPLAY: AtomicU16 = AtomicU16::new(32);

/// Coordinates on the value of `DISPLAY` to use when creating new X11 servers.
///
/// Methods on `DisplayNumberRegistry` may be called across threads or in
/// different processes to ensure that X servers are created with unique
/// display numbers.
pub struct DisplayNumberRegistry;

impl DisplayNumberRegistry {
    /// Returns a lock on the first local display number (the `N` in `:N` for
    /// the X client `DISPLAY` environment variable) that is not currently in
    /// use.
    ///
    /// If no display numbers are available, returns `None`.
    ///
    /// To avoid collisions with other processes, attempts are made to sniff out
    /// which display numbers are already in use by looking for lock files
    /// matching the pattern `/tmp/.X*-lock`. If such a file exists, its
    /// display number will not be used.
    ///
    /// When an available display number is found, an empty lockfile is
    /// atomically created (and marking the display number as claimed to other
    /// well-behaved processes). When `Xvfb` is run using that display number,
    /// it silently overwrites the empty lockfile.
    ///
    /// Any extant lockfile should be deleted by the `Drop` impl for [`Lock`],
    /// although tests that panic may leave stale lockfiles behind.
    pub fn next_unused_display() -> io::Result<Lock> {
        loop {
            let prev = NEXT_DISPLAY.fetch_add(1, atomic::Ordering::SeqCst);
            if prev == u16::MAX {
                return Err(io::Error::other("display numbers exhausted; check /tmp/.X{n}-lock"));
            }
            let next = prev + 1;
            let path = PathBuf::from(format!("/tmp/.X{}-lock", next));
            match fs::OpenOptions::new()
                .write(true)
                .create_new(true)
                .open(&path) {
                    Ok(_) => return Ok(Lock::new(next, path)),
                    Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue,
                    Err(e) => return Err(e),
                }
        }
    }
}

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

    #[test]
    fn find_unused_display() {
        assert!(DisplayNumberRegistry::next_unused_display().is_ok());

        assert_ne!(
            DisplayNumberRegistry::next_unused_display()
                .unwrap()
                .display,
            DisplayNumberRegistry::next_unused_display()
                .unwrap()
                .display
        );
    }
}
