brlapi 0.4.1

Safe Rust bindings for the BrlAPI library
// SPDX-License-Identifier: LGPL-2.1

//! BrlAPI connection management

use crate::{error::BrlApiError, settings::ConnectionSettings};
use brlapi_sys::*;
use std::ptr;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

/// A thread-safe BrlAPI connection with automatic cleanup
///
/// This struct wraps a BrlAPI handle and file descriptor, ensuring that
/// `brlapi__closeConnection()` is called when the connection is dropped.
/// Uses the handle-based API for thread safety.
#[derive(Debug)]
pub struct Connection {
    handle: Box<brlapi_handle_t>,
    fd: brlapi_fileDescriptor,
}

impl Connection {
    /// Open a new BrlAPI connection with default settings
    ///
    /// # Errors
    ///
    /// Returns `BrlApiError::ConnectionTimeout` if the connection times out (default 10 seconds).
    /// Returns `BrlApiError::ConnectionRefused` if the BrlAPI daemon is not running or connection fails.
    pub fn open() -> Result<Self, BrlApiError> {
        Self::open_with_settings(None)
    }

    /// Open a new BrlAPI connection with custom settings
    ///
    /// # Errors
    ///
    /// Returns `BrlApiError::ConnectionTimeout` if the connection times out.
    /// Returns `BrlApiError::InvalidParameter` if the settings contain invalid parameters.
    /// Returns `BrlApiError::ConnectionRefused` if the BrlAPI daemon is not running or connection fails.
    pub fn open_with_settings(settings: Option<&ConnectionSettings>) -> Result<Self, BrlApiError> {
        let timeout = settings
            .map(|s| s.timeout())
            .unwrap_or(Duration::from_secs(10));

        // Use timeout-aware connection
        Self::open_with_timeout(settings, timeout)
    }

    /// Open a BrlAPI connection with a specific timeout
    fn open_with_timeout(
        settings: Option<&ConnectionSettings>,
        timeout: Duration,
    ) -> Result<Self, BrlApiError> {
        // Create channel for thread communication
        let (sender, receiver) = mpsc::channel();

        // Clone settings data to send to thread (avoiding raw pointer issues)
        let settings_clone = settings.cloned();

        // Spawn connection attempt in a separate thread
        let connection_thread = thread::spawn(move || {
            let result = Self::attempt_connection(settings_clone.as_ref());
            let _ = sender.send(result); // Ignore send errors (timeout might have occurred)
        });

        // Wait for connection result or timeout
        match receiver.recv_timeout(timeout) {
            Ok(result) => {
                // Connection thread completed, get the result
                connection_thread.join().unwrap_or(()); // Best effort to clean up thread
                result
            }
            Err(mpsc::RecvTimeoutError::Timeout) => {
                // Connection timed out - thread may still be running but we can't wait
                // The thread will be cleaned up when the process exits
                Err(BrlApiError::ConnectionTimeout)
            }
            Err(mpsc::RecvTimeoutError::Disconnected) => {
                // Thread panicked or channel was closed
                connection_thread.join().unwrap_or(());
                Err(BrlApiError::ChannelError)
            }
        }
    }

    /// Internal function to attempt BrlAPI connection (runs in separate thread)
    fn attempt_connection(settings: Option<&ConnectionSettings>) -> Result<Self, BrlApiError> {
        // Allocate handle storage
        // SAFETY: brlapi_getHandleSize() is a pure function that returns the required
        // size for a BrlAPI handle allocation. No parameters needed, always safe to call.
        let handle_size = unsafe { brlapi_getHandleSize() };
        let mut handle = vec![0u8; handle_size].into_boxed_slice();
        let handle_ptr = handle.as_mut_ptr() as *mut brlapi_handle_t;

        // Prepare C-compatible settings inside this thread
        let c_settings_data = match settings {
            Some(s) => Some(s.to_c_settings()?),
            None => None,
        };
        let c_settings_ptr = c_settings_data
            .as_ref()
            .map_or(ptr::null(), |(c_settings, _, _)| c_settings as *const _);

        // SAFETY: handle_ptr points to valid handle memory allocated with correct size from brlapi_getHandleSize().
        // c_settings_ptr is either null or points to valid brlapi_connectionSettings_t from to_c_settings().
        // Third parameter is null_mut() as we don't need the actual settings back.
        let fd = crate::brlapi_call!(unsafe { brlapi__openConnection(handle_ptr, c_settings_ptr, ptr::null_mut()) })?;

        // Convert boxed slice to Box<brlapi_handle_t>
        // SAFETY: handle was allocated with correct size from brlapi_getHandleSize() and contains
        // a valid BrlAPI handle after successful brlapi__openConnection(). The cast from
        // *mut [u8] to *mut brlapi_handle_t is safe because both have the same memory layout.
        let handle = unsafe { Box::from_raw(Box::into_raw(handle) as *mut brlapi_handle_t) };
        Ok(Connection { handle, fd })
    }

    /// Get the file descriptor for this connection
    ///
    /// This can be used for select/poll operations or other low-level operations.
    pub fn file_descriptor(&self) -> i32 {
        self.fd
    }

    /// Get a raw pointer to the internal handle (for advanced usage)
    ///
    /// # Safety
    /// The returned pointer is valid only as long as this Connection exists.
    /// Do not use this pointer after the Connection is dropped.
    /// Caller must ensure that all BrlAPI calls using this handle are synchronized properly.
    /// This function ensures the returned pointer is never null.
    pub unsafe fn handle_ptr(&self) -> *mut brlapi_handle_t {
        let ptr = self.handle.as_ref() as *const brlapi_handle_t as *mut brlapi_handle_t;
        debug_assert!(!ptr.is_null(), "Connection handle should never be null");
        ptr
    }
}

impl Drop for Connection {
    fn drop(&mut self) {
        // SAFETY: self.handle contains a valid BrlAPI handle from successful brlapi__openConnection().
        // brlapi__closeConnection is safe to call once per handle and we ensure it's only called once in Drop.
        unsafe {
            brlapi__closeConnection(self.handle.as_mut());
        }
    }
}