procref 0.1.0

Cross-platform process reference counting for shared service lifecycle management
Documentation
//! # procref - Cross-platform Process Reference Counting
//!
//! A library for managing shared service lifecycles across multiple processes
//! using kernel-level reference counting mechanisms.
//!
//! ## Platform Support
//!
//! | Platform | Mechanism | Auto-cleanup on crash |
//! |----------|-----------|----------------------|
//! | Linux | System V Semaphore + SEM_UNDO | ✅ Kernel auto-undo |
//! | macOS | Mach Port send rights | ✅ Kernel auto-release |
//! | Windows | Named Semaphore | ✅ Handle auto-close |
//!
//! ## Design Philosophy
//!
//! This library trusts the **kernel** to manage reference counts, not userspace files.
//! When a process crashes:
//! - The kernel automatically decrements the reference count
//! - No stale state accumulates
//! - No manual cleanup needed
//!
//! Files (like port hints) are just **hints** for optimization, not the source of truth.
//! The actual process state is always verified against reality.
//!
//! ## Lifecycle Callbacks
//!
//! The library provides hooks for business logic at key lifecycle points:
//! - `on_first_acquire`: Called when first client registers (count 0→1)
//! - `on_last_release`: Called when last client exits (count 1→0)
//! - `on_health_check`: Called to verify service is healthy
//! - `on_recover`: Called when service needs recovery
//!
//! ## Usage
//!
//! ```no_run
//! use procref::{SharedService, ServiceInfo};
//!
//! # async fn example() -> procref::Result<()> {
//! let service = SharedService::builder("my-database")
//!     .on_first_acquire(|| async {
//!         // Start the database process
//!         let port = 5432;
//!         let pid = start_database(port)?;
//!         Ok(ServiceInfo::new(pid, port))
//!     })
//!     .on_last_release(|info| async move {
//!         // Stop the database when last client exits
//!         procref::process::terminate(info.pid());
//!         Ok(())
//!     })
//!     .build()?;
//!
//! // Acquire a reference (starts service if first client)
//! let handle = service.acquire().await?;
//! println!("Service running on port {}", handle.info().port());
//!
//! // handle.drop() releases reference
//! // If last client, on_last_release is called
//! # Ok(())
//! # }
//! # fn start_database(_: u16) -> procref::Result<u32> { Ok(0) }
//! ```

#![warn(missing_docs)]

mod error;
mod platform;
mod service;
mod types;

pub use error::{Error, Result};
pub use platform::PlatformRefCounter;
pub use service::{ServiceHandle, SharedService, SharedServiceBuilder};
pub use types::ServiceInfo;

/// Core trait for kernel-level reference counting.
///
/// Implementations must guarantee that reference counts are automatically
/// decremented by the kernel when a process crashes or exits abnormally.
///
/// This is NOT a userspace reference count - it relies on OS primitives
/// that the kernel manages.
pub trait RefCounter: Send + Sync {
    /// Acquire a reference (increment count).
    ///
    /// Returns the count AFTER incrementing.
    /// First acquisition (0→1) should trigger on_first_acquire callback.
    fn acquire(&self) -> Result<u32>;

    /// Release a reference (decrement count).
    ///
    /// Returns the count AFTER decrementing.
    /// Last release (1→0) should trigger on_last_release callback.
    ///
    /// Note: This is called for graceful shutdown. On crash, the kernel
    /// handles the decrement automatically.
    fn release(&self) -> Result<u32>;

    /// Get current reference count.
    ///
    /// This may be slightly stale in multi-process scenarios.
    /// Use for diagnostics, not critical decisions.
    fn count(&self) -> Result<u32>;

    /// Try to acquire startup lock (for coordinating service startup).
    ///
    /// Returns true if lock acquired, false if another process holds it.
    /// This is non-blocking.
    fn try_lock(&self) -> Result<bool>;

    /// Release startup lock.
    fn unlock(&self) -> Result<()>;
}

/// Utility functions for process management.
pub mod process {
    /// Check if a process with given PID is alive.
    pub fn is_alive(pid: u32) -> bool {
        #[cfg(unix)]
        {
            unsafe { libc::kill(pid as i32, 0) == 0 }
        }

        #[cfg(windows)]
        {
            use windows_sys::Win32::Foundation::CloseHandle;
            use windows_sys::Win32::System::Threading::{
                OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
            };
            unsafe {
                let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
                if handle == 0 {
                    return false;
                }
                CloseHandle(handle);
                true
            }
        }

        #[cfg(not(any(unix, windows)))]
        {
            let _ = pid;
            false
        }
    }

    /// Send SIGTERM to a process (graceful shutdown).
    pub fn terminate(pid: u32) {
        #[cfg(unix)]
        unsafe {
            libc::kill(pid as i32, libc::SIGTERM);
        }

        #[cfg(windows)]
        {
            kill(pid);
        }
    }

    /// Send SIGKILL to a process (forceful shutdown).
    pub fn kill(pid: u32) {
        #[cfg(unix)]
        unsafe {
            libc::kill(pid as i32, libc::SIGKILL);
        }

        #[cfg(windows)]
        {
            use windows_sys::Win32::Foundation::CloseHandle;
            use windows_sys::Win32::System::Threading::{
                OpenProcess, TerminateProcess, PROCESS_TERMINATE,
            };
            unsafe {
                let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
                if handle != 0 {
                    TerminateProcess(handle, 0);
                    CloseHandle(handle);
                }
            }
        }
    }

    /// Gracefully stop a process: SIGTERM, wait, then SIGKILL if needed.
    pub fn stop(pid: u32, timeout_ms: u64) {
        terminate(pid);

        let start = std::time::Instant::now();
        while start.elapsed().as_millis() < timeout_ms as u128 {
            if !is_alive(pid) {
                return;
            }
            std::thread::sleep(std::time::Duration::from_millis(50));
        }

        kill(pid);
    }
}