procref 0.1.0

Cross-platform process reference counting for shared service lifecycle management
Documentation
//! macOS implementation using Mach Ports.
//!
//! Mach Ports are kernel objects with reference-counted rights:
//! - Send rights can be duplicated (reference counted by kernel)
//! - When a process crashes, kernel automatically releases all rights
//! - When all send rights are released, receive right holder gets notification
//!
//! For cross-process coordination, we use bootstrap services to share ports.

use crate::{Error, RefCounter, Result};
use std::ffi::CString;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Mutex;

// Mach types and constants
type MachPortT = u32;
type KernReturnT = i32;

const KERN_SUCCESS: KernReturnT = 0;
const MACH_PORT_NULL: MachPortT = 0;
const MACH_PORT_RIGHT_RECEIVE: i32 = 1;
const MACH_PORT_RIGHT_SEND: i32 = 0;

// Bootstrap constants
const BOOTSTRAP_SUCCESS: KernReturnT = 0;

extern "C" {
    fn mach_task_self() -> MachPortT;
    fn mach_port_allocate(task: MachPortT, right: i32, name: *mut MachPortT) -> KernReturnT;
    fn mach_port_insert_right(
        task: MachPortT,
        name: MachPortT,
        poly: MachPortT,
        poly_poly: i32,
    ) -> KernReturnT;
    fn mach_port_deallocate(task: MachPortT, name: MachPortT) -> KernReturnT;
    fn mach_port_mod_refs(
        task: MachPortT,
        name: MachPortT,
        right: i32,
        delta: i32,
    ) -> KernReturnT;
    fn mach_port_get_refs(
        task: MachPortT,
        name: MachPortT,
        right: i32,
        refs: *mut u32,
    ) -> KernReturnT;

    // Bootstrap services for cross-process port sharing
    fn bootstrap_port() -> MachPortT;
    fn bootstrap_check_in(
        bp: MachPortT,
        service_name: *const i8,
        sp: *mut MachPortT,
    ) -> KernReturnT;
    fn bootstrap_look_up(
        bp: MachPortT,
        service_name: *const i8,
        sp: *mut MachPortT,
    ) -> KernReturnT;
    fn bootstrap_register(
        bp: MachPortT,
        service_name: *const i8,
        sp: MachPortT,
    ) -> KernReturnT;
}

/// macOS reference counter using Mach Ports.
///
/// Uses bootstrap services to share a Mach port across processes.
/// Each client acquires a send right, which the kernel tracks and
/// automatically releases on process exit.
pub struct MacOSRefCounter {
    /// Our send right to the shared port
    send_right: Mutex<Option<MachPortT>>,
    /// Whether we're the owner (have receive right)
    is_owner: AtomicBool,
    /// Receive right (only if owner)
    receive_right: Mutex<Option<MachPortT>>,
    /// Reference count (tracked locally for fast access)
    local_count: AtomicU32,
    /// Whether we have a reference
    has_reference: AtomicBool,
    /// Whether we hold the lock
    has_lock: AtomicBool,
    /// Service name
    name: String,
}

impl MacOSRefCounter {
    /// Create a new reference counter for the given service name.
    pub fn new(name: &str) -> Result<Self> {
        let service_name = format!("com.procref.{}", name);
        let c_name = CString::new(service_name.clone())
            .map_err(|e| Error::RefCounterInit(format!("Invalid service name: {}", e)))?;

        let mut counter = Self {
            send_right: Mutex::new(None),
            is_owner: AtomicBool::new(false),
            receive_right: Mutex::new(None),
            local_count: AtomicU32::new(0),
            has_reference: AtomicBool::new(false),
            has_lock: AtomicBool::new(false),
            name: name.to_string(),
        };

        // Try to look up existing service
        let mut port: MachPortT = MACH_PORT_NULL;
        let bp = unsafe { bootstrap_port() };

        let kr = unsafe { bootstrap_look_up(bp, c_name.as_ptr(), &mut port) };

        if kr == BOOTSTRAP_SUCCESS && port != MACH_PORT_NULL {
            // Service exists, we got a send right
            *counter.send_right.lock().unwrap() = Some(port);
        } else {
            // Service doesn't exist, create it
            counter.create_service(&c_name)?;
        }

        Ok(counter)
    }

    /// Create a new Mach port and register it with bootstrap.
    fn create_service(&mut self, c_name: &CString) -> Result<()> {
        let task = unsafe { mach_task_self() };
        let mut port: MachPortT = MACH_PORT_NULL;

        // Allocate receive right
        let kr = unsafe { mach_port_allocate(task, MACH_PORT_RIGHT_RECEIVE, &mut port) };
        if kr != KERN_SUCCESS {
            return Err(Error::RefCounterInit(format!(
                "mach_port_allocate failed: {}",
                kr
            )));
        }

        // Insert send right
        let kr = unsafe {
            mach_port_insert_right(task, port, port, MACH_PORT_RIGHT_SEND)
        };
        if kr != KERN_SUCCESS {
            return Err(Error::RefCounterInit(format!(
                "mach_port_insert_right failed: {}",
                kr
            )));
        }

        // Register with bootstrap
        let bp = unsafe { bootstrap_port() };
        let kr = unsafe { bootstrap_register(bp, c_name.as_ptr(), port) };
        if kr != BOOTSTRAP_SUCCESS {
            // Registration failed, but we can still use the port locally
            // This might happen if we don't have permission
            tracing::warn!("bootstrap_register failed: {}, using local port", kr);
        }

        *self.receive_right.lock().unwrap() = Some(port);
        *self.send_right.lock().unwrap() = Some(port);
        self.is_owner.store(true, Ordering::SeqCst);

        Ok(())
    }
}

impl RefCounter for MacOSRefCounter {
    fn acquire(&self) -> Result<u32> {
        if self.has_reference.load(Ordering::SeqCst) {
            return self.count();
        }

        let send_right = self.send_right.lock().unwrap();
        if let Some(port) = *send_right {
            // Add a send right reference
            let task = unsafe { mach_task_self() };
            let kr = unsafe { mach_port_mod_refs(task, port, MACH_PORT_RIGHT_SEND, 1) };
            if kr != KERN_SUCCESS {
                return Err(Error::Acquire(format!("mach_port_mod_refs failed: {}", kr)));
            }
        }

        self.has_reference.store(true, Ordering::SeqCst);
        let count = self.local_count.fetch_add(1, Ordering::SeqCst) + 1;
        Ok(count)
    }

    fn release(&self) -> Result<u32> {
        if !self.has_reference.load(Ordering::SeqCst) {
            return self.count();
        }

        let send_right = self.send_right.lock().unwrap();
        if let Some(port) = *send_right {
            // Release a send right reference
            let task = unsafe { mach_task_self() };
            let kr = unsafe { mach_port_mod_refs(task, port, MACH_PORT_RIGHT_SEND, -1) };
            if kr != KERN_SUCCESS {
                return Err(Error::Release(format!("mach_port_mod_refs failed: {}", kr)));
            }
        }

        self.has_reference.store(false, Ordering::SeqCst);
        let count = self.local_count.fetch_sub(1, Ordering::SeqCst) - 1;
        Ok(count)
    }

    fn count(&self) -> Result<u32> {
        // Try to get actual send right count from kernel
        let send_right = self.send_right.lock().unwrap();
        if let Some(port) = *send_right {
            let task = unsafe { mach_task_self() };
            let mut refs: u32 = 0;
            let kr = unsafe { mach_port_get_refs(task, port, MACH_PORT_RIGHT_SEND, &mut refs) };
            if kr == KERN_SUCCESS {
                return Ok(refs);
            }
        }

        // Fall back to local count
        Ok(self.local_count.load(Ordering::SeqCst))
    }

    fn try_lock(&self) -> Result<bool> {
        // For macOS, we use a simple file-based lock as Mach ports
        // don't directly support mutex semantics
        // This is a simplified implementation
        if self.has_lock.load(Ordering::SeqCst) {
            return Ok(true);
        }

        // Use file lock for startup coordination
        let lock_path = format!("/tmp/procref-{}.lock", self.name);
        let file = std::fs::OpenOptions::new()
            .write(true)
            .create(true)
            .open(&lock_path)
            .map_err(|e| Error::Lock(format!("Failed to open lock file: {}", e)))?;

        use std::os::unix::fs::FileExt;
        // Try non-blocking exclusive lock
        let result = unsafe {
            libc::flock(
                std::os::unix::io::AsRawFd::as_raw_fd(&file),
                libc::LOCK_EX | libc::LOCK_NB,
            )
        };

        if result == 0 {
            self.has_lock.store(true, Ordering::SeqCst);
            // Keep file handle to maintain lock - leak intentionally
            std::mem::forget(file);
            Ok(true)
        } else {
            Ok(false)
        }
    }

    fn unlock(&self) -> Result<()> {
        if !self.has_lock.load(Ordering::SeqCst) {
            return Ok(());
        }

        // File lock is released when file is closed
        // Since we leaked the file in try_lock, we need to reopen and close
        let lock_path = format!("/tmp/procref-{}.lock", self.name);
        if let Ok(file) = std::fs::File::open(&lock_path) {
            unsafe {
                libc::flock(
                    std::os::unix::io::AsRawFd::as_raw_fd(&file),
                    libc::LOCK_UN,
                );
            }
        }

        self.has_lock.store(false, Ordering::SeqCst);
        Ok(())
    }
}

impl Drop for MacOSRefCounter {
    fn drop(&mut self) {
        // Release lock if held
        if self.has_lock.load(Ordering::SeqCst) {
            let _ = self.unlock();
        }

        // Deallocate port if we're the owner
        if self.is_owner.load(Ordering::SeqCst) {
            if let Some(port) = *self.receive_right.lock().unwrap() {
                let task = unsafe { mach_task_self() };
                unsafe { mach_port_deallocate(task, port) };
            }
        }
    }
}

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

    #[test]
    fn test_macos_ref_counter_basic() {
        let counter = MacOSRefCounter::new("test-procref-macos").unwrap();

        // Acquire
        let count = counter.acquire().unwrap();
        assert!(count >= 1);

        // Release
        let count = counter.release().unwrap();
        assert!(count >= 0);
    }
}