pidlock/
lib.rs

1//! # pidlock
2//!
3//! A library for creating and managing PID-based file locks, providing a simple and reliable
4//! way to ensure only one instance of a program runs at a time.
5//!
6//! ## Features
7//!
8//! - **Cross-platform**: Works on Unix-like systems and Windows
9//! - **Stale lock detection**: Automatically detects and cleans up locks from dead processes
10//! - **Path validation**: Ensures lock file paths are valid across platforms
11//! - **Safe cleanup**: Automatically releases locks when the `Pidlock` is dropped
12//! - **Comprehensive error handling**: Detailed error types for different failure scenarios
13//!
14//! ## Quick Start
15//!
16//! ```rust
17//! use pidlock::Pidlock;
18//! use std::path::Path;
19//!
20//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
21//! let temp_dir = std::env::temp_dir();
22//! let lock_path = temp_dir.join("my_app.pid");
23//! let mut lock = Pidlock::new_validated(&lock_path)?;
24//!
25//! // Try to acquire the lock
26//! match lock.acquire() {
27//!     Ok(()) => {
28//!         println!("Lock acquired successfully!");
29//!         
30//!         // Do your work here...
31//!         
32//!         // Explicitly release the lock (optional - it's auto-released on drop)
33//!         lock.release()?;
34//!         println!("Lock released successfully!");
35//!     }
36//!     Err(pidlock::PidlockError::LockExists) => {
37//!         println!("Another instance is already running");
38//!     }
39//!     Err(e) => {
40//!         eprintln!("Failed to acquire lock: {}", e);
41//!     }
42//! }
43//! # Ok(())
44//! # }
45//! ```
46//!
47//! ## Advanced Usage
48//!
49//! ### Checking Lock Status Without Acquiring
50//!
51//! ```rust
52//! use pidlock::Pidlock;
53//!
54//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
55//! let temp_dir = std::env::temp_dir();
56//! let lock_path = temp_dir.join("example.pid");
57//! let lock = Pidlock::new_validated(&lock_path)?;
58//!
59//! // Check if a lock file exists
60//! if lock.exists() {
61//!     // Check if the lock is held by an active process
62//!     match lock.is_active()? {
63//!         true => println!("Lock is held by an active process"),
64//!         false => println!("Lock file exists but process is dead (stale lock)"),
65//!     }
66//! } else {
67//!     println!("No lock file exists");
68//! }
69//! # Ok(())
70//! # }
71//! ```
72//!
73//! ### Error Handling
74//!
75//! ```rust
76//! use pidlock::{Pidlock, PidlockError, InvalidPathError};
77//!
78//! # fn main() {
79//! let result = Pidlock::new_validated("invalid<path>");
80//! match result {
81//!     Ok(_) => println!("Path is valid"),
82//!     Err(PidlockError::InvalidPath(InvalidPathError::ProblematicCharacter { character, filename })) => {
83//!         println!("Invalid character '{}' in filename: {}", character, filename);
84//!     }
85//!     Err(e) => println!("Other error: {}", e),
86//! }
87//! # }
88//! ```
89//!
90//! ## Platform Considerations
91//!
92//! - **Unix/Linux**: Uses POSIX signals for process detection, respects umask for permissions
93//! - **Windows**: Uses Win32 APIs for process detection, handles reserved filenames
94//! - **File permissions**: Lock files are created with restrictive permissions (600 on Unix)
95//! - **Path validation**: Automatically validates paths for cross-platform compatibility
96//! - **Lock file locations**: Use `/run/lock/` on Linux, `/var/run/` on other Unix systems,
97//!   or appropriate system directories. Avoid `/tmp` for production use.
98//!
99//! ## Safety
100//!
101//! This library uses unsafe code for platform-specific process detection, but all unsafe
102//! operations are carefully validated and documented. The library ensures that:
103//!
104//! - PID values are validated before use in system calls
105//! - Windows handles are properly managed and cleaned up
106//! - Unix signals are used safely without affecting target processes
107
108use std::io::{Read, Write};
109use std::path::{Path, PathBuf};
110use std::{fs, process};
111
112use thiserror::Error;
113
114#[cfg(feature = "log")]
115use log::warn;
116
117/// Specific types of path validation errors.
118///
119/// These errors occur when the provided path for a lock file is not suitable
120/// for cross-platform use or contains problematic characters.
121///
122/// # Examples
123///
124/// ```rust
125/// use pidlock::{Pidlock, PidlockError, InvalidPathError};
126///
127/// // Example of catching specific path validation errors
128/// match Pidlock::new_validated("") {
129///     Err(PidlockError::InvalidPath(InvalidPathError::EmptyPath)) => {
130///         println!("Path cannot be empty");
131///     }
132///     _ => {}
133/// }
134/// ```
135#[derive(Debug, Error)]
136pub enum InvalidPathError {
137    #[error("Path cannot be empty")]
138    EmptyPath,
139
140    #[error("Filename '{filename}' is reserved on Windows")]
141    ReservedName { filename: String },
142
143    #[error("Filename contains problematic character '{character}': {filename}")]
144    ProblematicCharacter { character: char, filename: String },
145
146    #[error("Filename contains control characters: {filename}")]
147    ControlCharacters { filename: String },
148
149    #[error("Cannot create parent directory {path}")]
150    ParentDirectoryCreationFailed {
151        path: String,
152        #[source]
153        source: std::io::Error,
154    },
155}
156
157/// Errors that may occur during the `Pidlock` lifetime.
158///
159/// This enum covers all possible error conditions that can occur when working
160/// with pidlocks, from path validation to I/O errors during lock operations.
161///
162/// # Examples
163///
164/// ```rust
165/// use pidlock::{Pidlock, PidlockError};
166///
167/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
168/// let temp_dir = std::env::temp_dir();
169/// let lock_path = temp_dir.join("error_example.pid");
170/// let mut lock = Pidlock::new_validated(&lock_path)?;
171///
172/// match lock.acquire() {
173///     Ok(()) => {
174///         println!("Lock acquired successfully");
175///         lock.release()?;
176///     }
177///     Err(PidlockError::LockExists) => {
178///         println!("Another process is holding the lock");
179///     }
180///     Err(PidlockError::InvalidState) => {
181///         println!("Lock is in wrong state for this operation");
182///     }
183///     Err(e) => {
184///         println!("Other error: {}", e);
185///     }
186/// }
187/// # Ok(())
188/// # }
189/// ```
190#[derive(Debug, Error)]
191#[non_exhaustive]
192pub enum PidlockError {
193    #[error("A lock already exists")]
194    LockExists,
195
196    #[error("An operation was attempted in the wrong state, e.g. releasing before acquiring")]
197    InvalidState,
198
199    #[error("An I/O error occurred")]
200    IOError(#[from] std::io::Error),
201
202    #[error("Invalid path provided for lock file")]
203    InvalidPath(#[from] InvalidPathError),
204}
205
206impl PartialEq for PidlockError {
207    fn eq(&self, other: &Self) -> bool {
208        match (self, other) {
209            (PidlockError::LockExists, PidlockError::LockExists) => true,
210            (PidlockError::InvalidState, PidlockError::InvalidState) => true,
211            (PidlockError::IOError(a), PidlockError::IOError(b)) => {
212                // Compare IO errors by their kind only (more reliable than string comparison)
213                a.kind() == b.kind()
214            }
215            (PidlockError::InvalidPath(a), PidlockError::InvalidPath(b)) => {
216                // Compare InvalidPathError by discriminant only for now
217                // This is a simplified comparison since InvalidPathError may contain non-comparable fields
218                std::mem::discriminant(a) == std::mem::discriminant(b)
219            }
220            _ => false,
221        }
222    }
223}
224
225/// A result from a Pidlock operation
226type PidlockResult = Result<(), PidlockError>;
227
228/// States a Pidlock can be in during its lifetime.
229#[derive(Debug, PartialEq)]
230enum PidlockState {
231    #[doc = "A new pidlock, unacquired"]
232    New,
233    #[doc = "A lock is acquired"]
234    Acquired,
235    #[doc = "A lock is released"]
236    Released,
237}
238
239/// Validates that a path is suitable for use as a lock file.
240/// Checks for common cross-platform path issues.
241fn validate_lock_path(path: &Path) -> Result<(), PidlockError> {
242    #[cfg(feature = "log")]
243    if path.is_relative() {
244        warn!(
245            "Using relative path for lock file: {:?}. Consider using absolute paths for better reliability.",
246            path
247        );
248    }
249
250    // Check for empty path
251    if path.as_os_str().is_empty() {
252        return Err(PidlockError::InvalidPath(InvalidPathError::EmptyPath));
253    }
254
255    // Check for common problematic characters in filename
256    if let Some(filename) = path.file_name() {
257        let filename_str = filename.to_string_lossy();
258
259        // Check for reserved names on Windows
260        #[cfg(target_os = "windows")]
261        {
262            let reserved_names = [
263                "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
264                "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8",
265                "LPT9",
266            ];
267            let base_name = filename_str
268                .split('.')
269                .next()
270                .unwrap_or(&filename_str)
271                .to_uppercase();
272            if reserved_names.contains(&base_name.as_str()) {
273                return Err(PidlockError::InvalidPath(InvalidPathError::ReservedName {
274                    filename: filename_str.to_string(),
275                }));
276            }
277        }
278
279        // Check for problematic characters
280        let problematic_chars = ['<', '>', ':', '"', '|', '?', '*'];
281        for &ch in &problematic_chars {
282            if filename_str.contains(ch) {
283                return Err(PidlockError::InvalidPath(
284                    InvalidPathError::ProblematicCharacter {
285                        character: ch,
286                        filename: filename_str.to_string(),
287                    },
288                ));
289            }
290        }
291
292        // Check for control characters
293        if filename_str.chars().any(|c| c.is_control()) {
294            return Err(PidlockError::InvalidPath(
295                InvalidPathError::ControlCharacters {
296                    filename: filename_str.to_string(),
297                },
298            ));
299        }
300    }
301
302    // Try to validate parent directory exists or can be created
303    if let Some(parent) = path.parent()
304        && !parent.exists()
305    {
306        // Check if we can potentially create the directory
307        if let Err(e) = fs::create_dir_all(parent) {
308            return Err(PidlockError::InvalidPath(
309                InvalidPathError::ParentDirectoryCreationFailed {
310                    path: parent.display().to_string(),
311                    source: e,
312                },
313            ));
314        }
315    }
316
317    Ok(())
318}
319
320/// Validates that a PID is within reasonable bounds for the current system.
321fn validate_pid(pid: i32) -> bool {
322    // PIDs should be positive
323    if pid <= 0 {
324        return false;
325    }
326
327    // Check against system-specific limits
328    #[cfg(target_os = "linux")]
329    {
330        // Try to read the actual maximum PID from /proc/sys/kernel/pid_max
331        // If that fails, fall back to the typical default of 2^22 (4194304)
332        let max_pid = match fs::read_to_string("/proc/sys/kernel/pid_max") {
333            Ok(content) => match content.trim().parse::<i32>() {
334                Ok(parsed_max) => parsed_max,
335                Err(_parse_err) => {
336                    #[cfg(feature = "log")]
337                    warn!(
338                        "Failed to parse /proc/sys/kernel/pid_max content '{}': {}, using default 4194304",
339                        content.trim(),
340                        _parse_err
341                    );
342                    4194304
343                }
344            },
345            Err(_read_err) => {
346                #[cfg(feature = "log")]
347                warn!(
348                    "Failed to read /proc/sys/kernel/pid_max: {}, using default 4194304",
349                    _read_err
350                );
351                4194304
352            }
353        };
354
355        pid <= max_pid
356    }
357
358    #[cfg(target_os = "macos")]
359    {
360        // macOS defines PID_MAX as 99999, but process IDs are not assigned
361        // to PID_MAX, so max pid is 99998.
362        pid < 99999
363    }
364
365    #[cfg(target_os = "windows")]
366    {
367        // Windows uses 32-bit process IDs
368        // pid <= i32::MAX will always return true
369        true
370    }
371
372    #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
373    {
374        // Conservative default for other Unix-like systems
375        pid <= 99999
376    }
377}
378
379/// Check whether a process exists, used to determine whether a pid file is stale.
380///
381/// This function uses platform-specific system calls to check process existence
382/// without sending signals or affecting the target process.
383///
384/// # Arguments
385/// * `pid` - Process ID to check. Must be a positive integer within platform limits.
386///
387/// # Returns
388/// * `true` if the process exists and is accessible
389/// * `false` if the process doesn't exist, has exited, or we lack permissions
390///
391/// # Safety
392/// This function is safe when called with validated PIDs because:
393/// - On Windows: Uses safe Win32 APIs with proper handle management and error checking
394/// - On Unix: Uses the POSIX null signal (sig=0) which only performs permission checks
395fn process_exists(pid: i32) -> bool {
396    // Validate PID range before any unsafe operations
397    if !validate_pid(pid) {
398        return false;
399    }
400
401    #[cfg(target_os = "windows")]
402    {
403        // SAFETY: The `windows` crate does not provide a completely safe interface. Rather,
404        // it provides a "more safe" interface. As such, there is no safe API for windows.
405        // We use Windows APIs according to their documented contracts:
406        // - OpenProcess is called with valid flags and a validated positive PID
407        // - We check return values before using handles
408        // - CloseHandle is always called to prevent resource leaks
409        // - GetExitCodeProcess is only called with a valid handle
410        // The PID has already been validated by validate_pid() to be positive and within range
411        use windows::Win32::Foundation::{CloseHandle, HANDLE, NTSTATUS, STILL_ACTIVE};
412        use windows::Win32::System::Threading::{
413            GetExitCodeProcess, OpenProcess, PROCESS_QUERY_INFORMATION,
414        };
415
416        let handle = unsafe {
417            match OpenProcess(PROCESS_QUERY_INFORMATION, false, pid as u32) {
418                Ok(h) => h,
419                Err(_) => {
420                    // OpenProcess failed, likely due to invalid PID or insufficient permissions
421                    return false;
422                }
423            }
424        };
425
426        // Check if OpenProcess failed (returns 0 or INVALID_HANDLE_VALUE)
427        if handle.is_invalid() {
428            // Process doesn't exist or we don't have permission to query it
429            return false;
430        }
431
432        // Use RAII-style cleanup to ensure handle is always closed, even on panic
433        struct HandleGuard(HANDLE);
434        impl Drop for HandleGuard {
435            fn drop(&mut self) {
436                let _ = unsafe { CloseHandle(self.0) };
437            }
438        }
439        let _guard = HandleGuard(handle);
440
441        let mut exit_code: u32 = 0;
442        unsafe {
443            match GetExitCodeProcess(handle, &mut exit_code) {
444                Ok(_) => {
445                    // Return true only if GetExitCodeProcess succeeded AND process is still active
446                    // Note: STILL_ACTIVE (259) could theoretically be a real exit code, but it's
447                    // extremely unlikely in practice. This is the documented Windows API pattern
448                    // for checking if a process is still running. The risk of false positives
449                    // (a process that actually exited with code 259) is negligible.
450                    NTSTATUS(exit_code as i32) == STILL_ACTIVE
451                }
452                Err(_) => false, // GetExitCodeProcess failed
453            }
454        }
455    }
456
457    #[cfg(not(target_os = "windows"))]
458    {
459        // We specify None as the signal, which equates to using 0 in `kill(2)`. This
460        // means no signal is sent, but error checking is still performed.
461        nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid), None).is_ok()
462    }
463}
464
465/// A pid-centered lock. A lock is considered "acquired" when a file exists on disk
466/// at the path specified, containing the process id of the locking process.
467///
468/// ## Examples
469///
470/// ### Basic Usage
471///
472/// ```rust
473/// use pidlock::Pidlock;
474/// use std::fs;
475///
476/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
477/// // Create a lock in a temporary location
478/// let temp_dir = std::env::temp_dir();
479/// let lock_path = temp_dir.join("example.pid");
480///
481/// let mut lock = Pidlock::new_validated(&lock_path)?;
482///
483/// // Acquire the lock
484/// lock.acquire()?;
485/// assert!(lock.locked());
486///
487/// // The lock file now exists and contains our PID
488/// assert!(lock.exists());
489///
490/// // Release the lock
491/// lock.release()?;
492/// assert!(!lock.locked());
493/// assert!(!lock.exists()); // File is removed
494/// # Ok(())
495/// # }
496/// ```
497///
498/// ### Handling Lock Conflicts
499///
500/// ```rust
501/// use pidlock::{Pidlock, PidlockError};
502/// use std::fs;
503///
504/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
505/// let temp_dir = std::env::temp_dir();
506/// let lock_path = temp_dir.join("conflict_example.pid");
507///
508/// // First lock
509/// let mut lock1 = Pidlock::new_validated(&lock_path)?;
510/// lock1.acquire()?;
511///
512/// // Try to acquire the same lock from another instance
513/// let mut lock2 = Pidlock::new_validated(&lock_path)?;
514/// match lock2.acquire() {
515///     Err(PidlockError::LockExists) => {
516///         println!("Lock is already held by another process");
517///         // This is expected behavior
518///     }
519///     _ => panic!("Should have failed with LockExists"),
520/// }
521///
522/// // Clean up
523/// lock1.release()?;
524/// # Ok(())
525/// # }
526/// ```
527///
528/// ### Automatic Cleanup on Drop
529///
530/// ```rust
531/// use pidlock::Pidlock;
532/// use std::fs;
533///
534/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
535/// let temp_dir = std::env::temp_dir();
536/// let lock_path = temp_dir.join("drop_example.pid");
537///
538/// {
539///     let mut lock = Pidlock::new_validated(&lock_path)?;
540///     lock.acquire()?;
541///     assert!(lock.exists());
542///     // Lock goes out of scope here and is automatically cleaned up
543/// }
544///
545/// // Lock file should be removed by the Drop implementation
546/// assert!(!lock_path.exists());
547/// # Ok(())
548/// # }
549/// ```
550#[derive(Debug)]
551pub struct Pidlock {
552    #[doc = "The current process id"]
553    pid: u32,
554    #[doc = "A path to the lock file"]
555    path: PathBuf,
556    #[doc = "Current state of the Pidlock"]
557    state: PidlockState,
558}
559
560impl Pidlock {
561    /// Create a new Pidlock at the provided path.
562    ///
563    /// For backwards compatibility, this method does not validate the path.
564    /// Use `new_validated` if you want path validation.
565    #[deprecated(
566        since = "0.2.0",
567        note = "Use `new_validated` for path validation and better cross-platform compatibility"
568    )]
569    pub fn new(path: impl AsRef<Path>) -> Self {
570        Pidlock {
571            pid: process::id(),
572            path: path.as_ref().into(),
573            state: PidlockState::New,
574        }
575    }
576
577    /// Create a new Pidlock at the provided path with path validation.
578    ///
579    /// This is the recommended way to create a `Pidlock` as it validates the path
580    /// for cross-platform compatibility and common issues.
581    ///
582    /// # Arguments
583    ///
584    /// * `path` - The path where the lock file will be created. The parent directory
585    ///   must exist or be creatable.
586    ///
587    /// # Returns
588    ///
589    /// * `Ok(Pidlock)` - A new pidlock instance ready to be acquired
590    /// * `Err(PidlockError::InvalidPath)` - If the path is not suitable for use as a lock file
591    ///
592    /// # Examples
593    ///
594    /// ```rust
595    /// use pidlock::Pidlock;
596    /// use std::env;
597    ///
598    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
599    /// // Valid path
600    /// let temp_dir = env::temp_dir();
601    /// let lock_path = temp_dir.join("valid.pid");
602    /// let lock = Pidlock::new_validated(&lock_path)?;
603    /// # Ok(())
604    /// # }
605    /// ```
606    ///
607    /// ```rust
608    /// use pidlock::{Pidlock, PidlockError, InvalidPathError};
609    ///
610    /// # fn main() {
611    /// // Invalid path with problematic characters
612    /// let result = Pidlock::new_validated("invalid<file.pid");
613    /// match result {
614    ///     Err(PidlockError::InvalidPath(InvalidPathError::ProblematicCharacter { .. })) => {
615    ///         // Expected error for invalid characters
616    ///     }
617    ///     _ => panic!("Should have failed with InvalidPath"),
618    /// }
619    /// # }
620    /// ```
621    ///
622    /// # Errors
623    ///
624    /// Returns `PidlockError::InvalidPath` if the path is not suitable for use as a lock file.
625    pub fn new_validated(path: impl AsRef<Path>) -> Result<Self, PidlockError> {
626        let path_ref = path.as_ref();
627
628        // Validate the path before creating the Pidlock
629        validate_lock_path(path_ref)?;
630
631        Ok(Pidlock {
632            pid: process::id(),
633            path: path_ref.into(),
634            state: PidlockState::New,
635        })
636    }
637
638    /// Check whether a lock file already exists, and if it does, whether the
639    /// specified pid is still a valid process id on the system.
640    /// Returns true if the lock exists but the process is no longer running.
641    fn check_stale(&self) -> bool {
642        // First check if the lock file even exists
643        if !self.path.exists() {
644            return false;
645        }
646
647        // Try to get the owner PID - if this fails, we can't determine if it's stale
648        match self.get_owner() {
649            Ok(Some(pid)) => {
650                // We have a valid PID, check if the process is still running
651                !process_exists(pid)
652            }
653            Ok(None) => {
654                // No PID found in file, consider it stale
655                true
656            }
657            Err(_) => {
658                // Error reading the file, can't determine staleness safely
659                false
660            }
661        }
662    }
663
664    /// Acquire a lock.
665    ///
666    /// This method attempts to create the lock file containing the current process ID.
667    /// If a stale lock file exists (from a dead process), it will be automatically cleaned up.
668    ///
669    /// # Returns
670    ///
671    /// * `Ok(())` - Lock was successfully acquired
672    /// * `Err(PidlockError::LockExists)` - Another active process holds the lock
673    /// * `Err(PidlockError::InvalidState)` - Lock is already acquired or released
674    /// * `Err(PidlockError::IOError)` - File system error occurred
675    ///
676    /// # Examples
677    ///
678    /// ```rust
679    /// use pidlock::{Pidlock, PidlockError};
680    /// use std::env;
681    ///
682    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
683    /// let temp_dir = env::temp_dir();
684    /// let lock_path = temp_dir.join("acquire_example.pid");
685    ///
686    /// let mut lock = Pidlock::new_validated(&lock_path)?;
687    ///
688    /// match lock.acquire() {
689    ///     Ok(()) => {
690    ///         println!("Lock acquired successfully!");
691    ///         // Do your work here...
692    ///         lock.release()?;
693    ///     }
694    ///     Err(PidlockError::LockExists) => {
695    ///         println!("Another instance is already running");
696    ///     }
697    ///     Err(e) => {
698    ///         eprintln!("Unexpected error: {}", e);
699    ///     }
700    /// }
701    /// # Ok(())
702    /// # }
703    /// ```
704    ///
705    /// # Behavior
706    ///
707    /// 1. **State validation**: Can only be called on a `New` pidlock
708    /// 2. **Stale lock cleanup**: Automatically removes locks from dead processes
709    /// 3. **Atomic creation**: Uses `O_EXCL`/`CREATE_NEW` for atomic lock file creation
710    /// 4. **Secure permissions**: Creates files with restrictive permissions (600 on Unix)
711    /// 5. **Reliable writes**: Flushes data to disk before returning success
712    pub fn acquire(&mut self) -> PidlockResult {
713        match self.state {
714            PidlockState::New => {}
715            _ => {
716                return Err(PidlockError::InvalidState);
717            }
718        }
719
720        // Check if there's a stale lock that we can remove
721        if self.check_stale() {
722            // Lock exists but process is dead, remove the stale lock file
723            let _ = fs::remove_file(&self.path);
724        }
725
726        // Create file with appropriate permissions
727        let mut options = fs::OpenOptions::new();
728        options.create_new(true).write(true);
729
730        // Set restrictive permissions on Unix-like systems for security
731        #[cfg(unix)]
732        {
733            use std::os::unix::fs::OpenOptionsExt;
734            options.mode(0o600);
735        }
736
737        let mut file = match options.open(&self.path) {
738            Ok(file) => file,
739            Err(err) => {
740                return match err.kind() {
741                    std::io::ErrorKind::AlreadyExists => Err(PidlockError::LockExists),
742                    _ => Err(PidlockError::from(err)),
743                };
744            }
745        };
746
747        file.write_all(&self.pid.to_string().into_bytes()[..])
748            .map_err(PidlockError::from)?;
749
750        // Ensure data is written to disk for reliability
751        file.flush().map_err(PidlockError::from)?;
752
753        self.state = PidlockState::Acquired;
754        Ok(())
755    }
756
757    /// Returns true when the lock is in an acquired state.
758    ///
759    /// This is a local state check only - it tells you whether this `Pidlock` instance
760    /// has successfully acquired a lock, but doesn't check if the lock file still exists
761    /// on disk or if another process might have interfered with it.
762    ///
763    /// # Returns
764    ///
765    /// `true` if this `Pidlock` instance has acquired the lock, `false` otherwise.
766    ///
767    /// # Examples
768    ///
769    /// ```rust
770    /// use pidlock::Pidlock;
771    /// use std::env;
772    ///
773    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
774    /// let temp_dir = env::temp_dir();
775    /// let lock_path = temp_dir.join("locked_example.pid");
776    ///
777    /// let mut lock = Pidlock::new_validated(&lock_path)?;
778    ///
779    /// // Initially not locked
780    /// assert!(!lock.locked());
781    ///
782    /// // After acquiring
783    /// lock.acquire()?;
784    /// assert!(lock.locked());
785    ///
786    /// // After releasing
787    /// lock.release()?;
788    /// assert!(!lock.locked());
789    /// # Ok(())
790    /// # }
791    /// ```
792    ///
793    /// # Note
794    ///
795    /// For checking if a lock file exists on disk, use [`exists()`](Self::exists).
796    /// For checking if a lock is held by an active process, use [`is_active()`](Self::is_active).
797    pub fn locked(&self) -> bool {
798        self.state == PidlockState::Acquired
799    }
800
801    /// Check if the lock file exists on disk.
802    /// This is a read-only operation that doesn't modify the lock state.
803    ///
804    /// # Returns
805    ///
806    /// `true` if the lock file exists, `false` otherwise.
807    ///
808    /// # Examples
809    ///
810    /// ```
811    /// use pidlock::Pidlock;
812    /// use std::env;
813    ///
814    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
815    /// let temp_dir = env::temp_dir();
816    /// let lock_path = temp_dir.join("exists_example.pid");
817    ///
818    /// let lock = Pidlock::new_validated(&lock_path)?;
819    ///
820    /// // Initially, no lock file exists
821    /// assert!(!lock.exists());
822    ///
823    /// // Create a lock file manually to test
824    /// std::fs::write(&lock_path, "12345")?;
825    /// assert!(lock.exists());
826    ///
827    /// // Clean up
828    /// std::fs::remove_file(&lock_path)?;
829    /// # Ok(())
830    /// # }
831    /// ```
832    pub fn exists(&self) -> bool {
833        self.path.exists()
834    }
835
836    /// Check if the lock file exists and if so, whether it's stale (owned by a dead process).
837    /// This is a read-only operation that doesn't modify the lock state.
838    ///
839    /// # Returns
840    ///
841    /// `Ok(true)` if a lock exists and the owning process is still running,
842    /// `Ok(false)` if no lock exists or the lock is stale,
843    /// `Err(_)` if there was an error determining the lock status.
844    ///
845    /// # Examples
846    ///
847    /// ```
848    /// use pidlock::Pidlock;
849    /// use std::env;
850    ///
851    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
852    /// let temp_dir = env::temp_dir();
853    /// let lock_path = temp_dir.join("is_active_example.pid");
854    ///
855    /// let lock = Pidlock::new_validated(&lock_path)?;
856    ///
857    /// match lock.is_active()? {
858    ///     true => println!("Lock is held by an active process"),
859    ///     false => println!("No active lock found"),
860    /// }
861    ///
862    /// // Test with our own process
863    /// std::fs::write(&lock_path, std::process::id().to_string())?;
864    /// assert!(lock.is_active()?); // Our process is definitely active
865    ///
866    /// // Clean up
867    /// std::fs::remove_file(&lock_path)?;
868    /// # Ok(())
869    /// # }
870    /// ```
871    pub fn is_active(&self) -> Result<bool, PidlockError> {
872        if !self.path.exists() {
873            return Ok(false);
874        }
875
876        match self.get_owner()? {
877            Some(pid) => Ok(process_exists(pid)),
878            None => Ok(false), // No PID in file means inactive
879        }
880    }
881
882    /// Release the lock.
883    ///
884    /// This method removes the lock file from disk and transitions the lock to the `Released` state.
885    /// Once released, the lock cannot be re-acquired (create a new `Pidlock` instance instead).
886    ///
887    /// # Returns
888    ///
889    /// * `Ok(())` - Lock was successfully released
890    /// * `Err(PidlockError::InvalidState)` - Lock is not currently acquired
891    /// * `Err(PidlockError::IOError)` - File system error occurred during removal
892    ///
893    /// # Examples
894    ///
895    /// ```rust
896    /// use pidlock::Pidlock;
897    /// use std::env;
898    ///
899    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
900    /// let temp_dir = env::temp_dir();
901    /// let lock_path = temp_dir.join("release_example.pid");
902    ///
903    /// let mut lock = Pidlock::new_validated(&lock_path)?;
904    /// lock.acquire()?;
905    ///
906    /// // Explicitly release the lock
907    /// lock.release()?;
908    ///
909    /// // Lock file should be gone
910    /// assert!(!lock.exists());
911    /// assert!(!lock.locked());
912    /// # Ok(())
913    /// # }
914    /// ```
915    ///
916    /// # Note
917    ///
918    /// Releasing a lock is optional - the `Drop` implementation will automatically
919    /// clean up acquired locks when the `Pidlock` goes out of scope. However, explicit
920    /// release is recommended for better error handling and immediate cleanup.
921    pub fn release(&mut self) -> PidlockResult {
922        match self.state {
923            PidlockState::Acquired => {}
924            _ => {
925                return Err(PidlockError::InvalidState);
926            }
927        }
928
929        fs::remove_file(&self.path).map_err(PidlockError::from)?;
930
931        self.state = PidlockState::Released;
932        Ok(())
933    }
934
935    /// Gets the owner of this lockfile, returning the PID if it exists and is valid.
936    ///
937    /// This method reads the lock file and attempts to parse the PID contained within.
938    /// If the PID is invalid, the process no longer exists, or the file is corrupted,
939    /// the lock file will be automatically cleaned up.
940    ///
941    /// # Returns
942    ///
943    /// * `Ok(Some(pid))` - Lock file exists and contains a valid PID for an active process
944    /// * `Ok(None)` - No lock file exists, or the lock file was invalid and cleaned up
945    /// * `Err(_)` - I/O error occurred while reading or cleaning up the file
946    ///
947    /// # Examples
948    ///
949    /// ```rust
950    /// use pidlock::Pidlock;
951    /// use std::env;
952    ///
953    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
954    /// let temp_dir = env::temp_dir();
955    /// let lock_path = temp_dir.join("owner_example.pid");
956    ///
957    /// let mut lock = Pidlock::new_validated(&lock_path)?;
958    ///
959    /// // No owner initially
960    /// assert_eq!(lock.get_owner()?, None);
961    ///
962    /// // After acquiring, we should be the owner
963    /// lock.acquire()?;
964    /// if let Some(owner_pid) = lock.get_owner()? {
965    ///     println!("Lock is owned by PID: {}", owner_pid);
966    ///     assert_eq!(owner_pid, std::process::id() as i32);
967    /// }
968    ///
969    /// lock.release()?;
970    /// # Ok(())
971    /// # }
972    /// ```
973    ///
974    /// # Behavior
975    ///
976    /// This method will automatically clean up lock files in the following cases:
977    /// - File contains non-numeric content
978    /// - File contains a PID that doesn't correspond to a running process
979    /// - File contains a PID outside the valid range for the platform
980    /// - File is empty or contains only whitespace
981    pub fn get_owner(&self) -> Result<Option<i32>, PidlockError> {
982        let mut file = match fs::OpenOptions::new().read(true).open(&self.path) {
983            Ok(file) => file,
984            Err(_) => {
985                return Ok(None);
986            }
987        };
988
989        let mut contents = String::new();
990        if file.read_to_string(&mut contents).is_err() {
991            #[cfg(feature = "log")]
992            warn!(
993                "Removing corrupted/invalid pid file at {}",
994                self.path.to_str().unwrap_or("<invalid>")
995            );
996            fs::remove_file(&self.path).map_err(PidlockError::from)?;
997            return Ok(None);
998        }
999
1000        match contents.trim().parse::<i32>() {
1001            Ok(pid) if validate_pid(pid) && process_exists(pid) => Ok(Some(pid)),
1002            Ok(_) => {
1003                #[cfg(feature = "log")]
1004                warn!(
1005                    "Removing stale pid file at {}",
1006                    self.path.to_str().unwrap_or("<invalid>")
1007                );
1008                fs::remove_file(&self.path).map_err(PidlockError::from)?;
1009                Ok(None)
1010            }
1011            Err(_) => {
1012                #[cfg(feature = "log")]
1013                warn!(
1014                    "Removing corrupted/invalid pid file at {}",
1015                    self.path.to_str().unwrap_or("<invalid>")
1016                );
1017                fs::remove_file(&self.path).map_err(PidlockError::from)?;
1018                Ok(None)
1019            }
1020        }
1021    }
1022}
1023
1024impl Drop for Pidlock {
1025    /// Automatically release the lock when the Pidlock goes out of scope.
1026    /// This ensures that lock files are cleaned up even if the process panics
1027    /// or exits unexpectedly while holding a lock.
1028    ///
1029    /// Note: This implementation uses a best-effort approach. If cleanup fails,
1030    /// we don't panic because that could mask the original panic that triggered
1031    /// the drop. Errors are logged when the `log` feature is enabled.
1032    fn drop(&mut self) {
1033        if self.state == PidlockState::Acquired {
1034            // Best-effort cleanup - we can't return errors from Drop
1035            match fs::remove_file(&self.path) {
1036                Ok(()) => {
1037                    #[cfg(feature = "log")]
1038                    log::debug!("Successfully cleaned up lock file: {:?}", self.path);
1039                }
1040                Err(e) => {
1041                    #[cfg(feature = "log")]
1042                    log::warn!(
1043                        "Failed to remove lock file {:?} during drop: {}. \
1044                         This may leave a stale lock file on disk.",
1045                        self.path,
1046                        e
1047                    );
1048
1049                    // Prevent unused variable warning when log feature is disabled
1050                    #[cfg(not(feature = "log"))]
1051                    let _ = e;
1052
1053                    // Silently ignore the error to avoid panicking during drop
1054                }
1055            }
1056        }
1057    }
1058}
1059
1060#[cfg(test)]
1061mod tests {
1062    use std::io::Write;
1063    use std::path::PathBuf;
1064
1065    use rand::distributions::Alphanumeric;
1066    use rand::{Rng, thread_rng};
1067    use tempfile::NamedTempFile;
1068
1069    use super::PidlockState;
1070    use super::{Pidlock, PidlockError};
1071
1072    fn make_temp_file() -> NamedTempFile {
1073        NamedTempFile::new().expect("Failed to create temporary file")
1074    }
1075
1076    #[test]
1077    fn test_new() {
1078        let temp_file = make_temp_file();
1079        let pid_path = temp_file.path().to_str().unwrap();
1080        let pidfile = Pidlock::new_validated(pid_path).unwrap();
1081
1082        assert_eq!(pidfile.pid, std::process::id());
1083        assert_eq!(pidfile.path, PathBuf::from(pid_path));
1084        assert_eq!(pidfile.state, PidlockState::New);
1085    }
1086
1087    #[test]
1088    fn test_acquire_and_release() {
1089        let temp_file = make_temp_file();
1090        let pid_path = temp_file.path().to_str().unwrap();
1091        let mut pidfile = Pidlock::new_validated(pid_path).unwrap();
1092        pidfile.acquire().unwrap();
1093
1094        assert_eq!(pidfile.state, PidlockState::Acquired);
1095
1096        pidfile.release().unwrap();
1097
1098        assert_eq!(pidfile.state, PidlockState::Released);
1099    }
1100
1101    #[test]
1102    fn test_acquire_lock_exists() {
1103        let temp_file = make_temp_file();
1104        let pid_path = temp_file.path().to_str().unwrap();
1105        let mut orig_pidfile = Pidlock::new_validated(pid_path).unwrap();
1106        orig_pidfile.acquire().unwrap();
1107
1108        let mut pidfile = Pidlock::new_validated(orig_pidfile.path.to_str().unwrap()).unwrap();
1109        match pidfile.acquire() {
1110            Err(err) => {
1111                orig_pidfile.release().unwrap();
1112                assert_eq!(err, PidlockError::LockExists);
1113            }
1114            _ => {
1115                orig_pidfile.release().unwrap();
1116                panic!("Test failed");
1117            }
1118        }
1119    }
1120
1121    #[test]
1122    fn test_acquire_already_acquired() {
1123        let temp_file = make_temp_file();
1124        let pid_path = temp_file.path().to_str().unwrap();
1125        let mut pidfile = Pidlock::new_validated(pid_path).unwrap();
1126        pidfile.acquire().unwrap();
1127        match pidfile.acquire() {
1128            Err(err) => {
1129                pidfile.release().unwrap();
1130                assert_eq!(err, PidlockError::InvalidState);
1131            }
1132            _ => {
1133                pidfile.release().unwrap();
1134                panic!("Test failed");
1135            }
1136        }
1137    }
1138
1139    #[test]
1140    fn test_release_bad_state() {
1141        let temp_file = make_temp_file();
1142        let pid_path = temp_file.path().to_str().unwrap();
1143        let mut pidfile = Pidlock::new_validated(pid_path).unwrap();
1144        match pidfile.release() {
1145            Err(err) => {
1146                assert_eq!(err, PidlockError::InvalidState);
1147            }
1148            _ => {
1149                panic!("Test failed");
1150            }
1151        }
1152    }
1153
1154    #[test]
1155    fn test_locked() {
1156        let temp_file = make_temp_file();
1157        let pid_path = temp_file.path().to_str().unwrap();
1158        let mut pidfile = Pidlock::new_validated(pid_path).unwrap();
1159        pidfile.acquire().unwrap();
1160        assert!(pidfile.locked());
1161    }
1162
1163    #[test]
1164    fn test_locked_not_locked() {
1165        let temp_file = make_temp_file();
1166        let pid_path = temp_file.path().to_str().unwrap();
1167        let pidfile = Pidlock::new_validated(pid_path).unwrap();
1168        assert!(!pidfile.locked());
1169    }
1170
1171    #[test]
1172    fn test_stale_pid() {
1173        let mut temp_file = make_temp_file();
1174        let path = temp_file.path().to_string_lossy().to_string();
1175
1176        // Write a random PID to the temp file
1177        temp_file
1178            .write_all(&format!("{}", thread_rng().r#gen::<i32>()).into_bytes()[..])
1179            .unwrap();
1180        temp_file.flush().unwrap();
1181
1182        let mut pidfile = Pidlock::new_validated(&path).unwrap();
1183        pidfile.acquire().unwrap();
1184        assert_eq!(pidfile.state, PidlockState::Acquired);
1185    }
1186
1187    #[test]
1188    fn test_stale_pid_invalid_contents() {
1189        let mut temp_file = make_temp_file();
1190        let path = temp_file.path().to_string_lossy().to_string();
1191
1192        let contents: String = thread_rng()
1193            .sample_iter(&Alphanumeric)
1194            .take(20)
1195            .map(char::from)
1196            .collect();
1197        temp_file.write_all(&contents.into_bytes()).unwrap();
1198        temp_file.flush().unwrap();
1199
1200        let mut pidfile = Pidlock::new_validated(&path).unwrap();
1201        pidfile.acquire().unwrap();
1202        assert_eq!(pidfile.state, PidlockState::Acquired);
1203    }
1204
1205    #[test]
1206    fn test_stale_pid_corrupted_contents() {
1207        let mut temp_file = make_temp_file();
1208        let path = temp_file.path().to_string_lossy().to_string();
1209
1210        temp_file
1211            .write_all(&rand::thread_rng().r#gen::<[u8; 32]>())
1212            .unwrap();
1213        temp_file.flush().unwrap();
1214
1215        let mut pidfile = Pidlock::new_validated(&path).unwrap();
1216        pidfile.acquire().unwrap();
1217        assert_eq!(pidfile.state, PidlockState::Acquired);
1218    }
1219
1220    #[test]
1221    fn test_drop_cleans_up_lock_file() {
1222        let temp_file = make_temp_file();
1223        let path = temp_file.path().to_string_lossy().to_string();
1224
1225        // Create and acquire a lock in a scope
1226        {
1227            let mut pidfile = Pidlock::new_validated(&path).unwrap();
1228            pidfile.acquire().unwrap();
1229            assert_eq!(pidfile.state, PidlockState::Acquired);
1230
1231            // Verify the lock file exists
1232            assert!(std::path::Path::new(&path).exists());
1233
1234            // The Drop implementation should clean up when pidfile goes out of scope
1235        }
1236
1237        // After the scope ends, the lock file should be cleaned up
1238        assert!(!std::path::Path::new(&path).exists());
1239    }
1240
1241    #[test]
1242    fn test_drop_only_cleans_up_when_acquired() {
1243        let temp_file = make_temp_file();
1244        let path = temp_file.path().to_string_lossy().to_string();
1245
1246        // Create a lock but don't acquire it
1247        {
1248            let _pidfile = Pidlock::new_validated(&path).unwrap();
1249            // Lock is not acquired, so drop should not try to remove anything
1250        }
1251
1252        // Should not have attempted to remove a non-existent lock file
1253        // (This test mainly ensures no panic occurs during drop)
1254
1255        // Now create a lock, acquire and manually release it
1256        {
1257            let mut pidfile = Pidlock::new_validated(&path).unwrap();
1258            pidfile.acquire().unwrap();
1259            pidfile.release().unwrap();
1260            assert_eq!(pidfile.state, PidlockState::Released);
1261
1262            // Drop should not try to clean up since state is Released
1263        }
1264
1265        // File should already be gone from manual release
1266        assert!(!std::path::Path::new(&path).exists());
1267    }
1268
1269    #[test]
1270    fn test_get_owner_no_file() {
1271        let temp_file = make_temp_file();
1272        let path = temp_file.path().to_string_lossy().to_string();
1273
1274        let pidfile = Pidlock::new_validated(&path).unwrap();
1275        let result = pidfile.get_owner().unwrap();
1276        assert_eq!(result, None);
1277    }
1278
1279    #[test]
1280    fn test_get_owner_valid_pid() {
1281        let temp_file = make_temp_file();
1282        let path = temp_file.path().to_string_lossy().to_string();
1283
1284        // First create a lock with our own PID
1285        let mut pidfile = Pidlock::new_validated(&path).unwrap();
1286        pidfile.acquire().unwrap();
1287
1288        // Now test get_owner returns our PID
1289        let result = pidfile.get_owner().unwrap();
1290        assert_eq!(result, Some(std::process::id() as i32));
1291
1292        pidfile.release().unwrap();
1293    }
1294
1295    #[test]
1296    fn test_get_owner_empty_file() {
1297        let mut temp_file = make_temp_file();
1298        let path = temp_file.path().to_string_lossy().to_string();
1299
1300        // Write empty content
1301        temp_file.write_all(b"").unwrap();
1302        temp_file.flush().unwrap();
1303
1304        let pidfile = Pidlock::new_validated(&path).unwrap();
1305        let result = pidfile.get_owner().unwrap();
1306        // Empty file should be cleaned up and return None
1307        assert_eq!(result, None);
1308        assert!(!std::path::Path::new(&path).exists());
1309    }
1310
1311    #[test]
1312    fn test_get_owner_whitespace_only() {
1313        let mut temp_file = make_temp_file();
1314        let path = temp_file.path().to_string_lossy().to_string();
1315
1316        // Write whitespace-only content
1317        temp_file.write_all(b"   \n  \t  \r\n  ").unwrap();
1318        temp_file.flush().unwrap();
1319
1320        let pidfile = Pidlock::new_validated(&path).unwrap();
1321        let result = pidfile.get_owner().unwrap();
1322        // Whitespace-only file should be cleaned up and return None
1323        assert_eq!(result, None);
1324        assert!(!std::path::Path::new(&path).exists());
1325    }
1326
1327    #[test]
1328    fn test_get_owner_negative_pid() {
1329        let mut temp_file = make_temp_file();
1330        let path = temp_file.path().to_string_lossy().to_string();
1331
1332        // Write a negative PID (which shouldn't exist)
1333        temp_file.write_all(b"-12345").unwrap();
1334        temp_file.flush().unwrap();
1335
1336        let pidfile = Pidlock::new_validated(&path).unwrap();
1337        let result = pidfile.get_owner().unwrap();
1338        // Negative PID should be cleaned up and return None
1339        assert_eq!(result, None);
1340        assert!(!std::path::Path::new(&path).exists());
1341    }
1342
1343    #[test]
1344    fn test_get_owner_large_pid() {
1345        let mut temp_file = make_temp_file();
1346        let path = temp_file.path().to_string_lossy().to_string();
1347
1348        // Write a very large PID (likely doesn't exist but valid i32)
1349        let large_pid = i32::MAX;
1350        temp_file
1351            .write_all(large_pid.to_string().as_bytes())
1352            .unwrap();
1353        temp_file.flush().unwrap();
1354
1355        let pidfile = Pidlock::new_validated(&path).unwrap();
1356        let result = pidfile.get_owner().unwrap();
1357        // Large PID should be cleaned up since it likely doesn't exist
1358        assert_eq!(result, None);
1359        assert!(!std::path::Path::new(&path).exists());
1360    }
1361
1362    #[test]
1363    fn test_get_owner_zero_pid() {
1364        let mut temp_file = make_temp_file();
1365        let path = temp_file.path().to_string_lossy().to_string();
1366
1367        // Write PID 0 (which may or may not exist depending on the system)
1368        temp_file.write_all(b"0").unwrap();
1369        temp_file.flush().unwrap();
1370
1371        let pidfile = Pidlock::new_validated(&path).unwrap();
1372        let result = pidfile.get_owner().unwrap();
1373
1374        // PID 0 behavior is system-dependent:
1375        // - On some systems (like macOS), PID 0 exists (kernel)
1376        // - On others, it may not
1377        // We just verify the method doesn't panic and returns a valid result
1378        match result {
1379            Some(0) => {
1380                // PID 0 exists on this system, file should remain
1381                assert!(std::path::Path::new(&path).exists());
1382            }
1383            None => {
1384                // PID 0 doesn't exist, file should be cleaned up
1385                assert!(!std::path::Path::new(&path).exists());
1386            }
1387            Some(other) => {
1388                panic!("Expected PID 0 or None, got Some({})", other);
1389            }
1390        }
1391    }
1392
1393    #[test]
1394    fn test_get_owner_mixed_content() {
1395        let mut temp_file = make_temp_file();
1396        let path = temp_file.path().to_string_lossy().to_string();
1397
1398        // Write PID with trailing content that should be ignored
1399        temp_file.write_all(b"12345 extra content").unwrap();
1400        temp_file.flush().unwrap();
1401
1402        let pidfile = Pidlock::new_validated(&path).unwrap();
1403        let result = pidfile.get_owner().unwrap();
1404        // Should parse the PID part and clean up since 12345 likely doesn't exist
1405        assert_eq!(result, None);
1406        assert!(!std::path::Path::new(&path).exists());
1407    }
1408
1409    #[test]
1410    fn test_concurrent_acquire_attempts() {
1411        let temp_file = make_temp_file();
1412        let path = temp_file.path().to_string_lossy().to_string();
1413
1414        // First lock should succeed
1415        let mut lock1 = Pidlock::new_validated(&path).unwrap();
1416        assert!(lock1.acquire().is_ok());
1417
1418        // Second lock should fail with LockExists
1419        let mut lock2 = Pidlock::new_validated(&path).unwrap();
1420        match lock2.acquire() {
1421            Err(PidlockError::LockExists) => {} // Expected
1422            other => panic!("Expected LockExists, got {:?}", other),
1423        }
1424
1425        // Clean up
1426        lock1.release().unwrap();
1427
1428        // Now second lock should succeed
1429        assert!(lock2.acquire().is_ok());
1430        lock2.release().unwrap();
1431    }
1432
1433    #[test]
1434    fn test_acquire_after_stale_cleanup() {
1435        let mut temp_file = make_temp_file();
1436        let path = temp_file.path().to_string_lossy().to_string();
1437
1438        // Write a definitely non-existent PID
1439        temp_file.write_all(b"999999").unwrap();
1440        temp_file.flush().unwrap();
1441
1442        // Acquiring should clean up the stale file and succeed
1443        let mut pidfile = Pidlock::new_validated(&path).unwrap();
1444        assert!(pidfile.acquire().is_ok());
1445        assert_eq!(pidfile.state, PidlockState::Acquired);
1446
1447        // Verify the file now contains our PID
1448        let owner = pidfile.get_owner().unwrap();
1449        assert_eq!(owner, Some(std::process::id() as i32));
1450
1451        pidfile.release().unwrap();
1452    }
1453
1454    #[test]
1455    fn test_new_validated_valid_path() {
1456        let temp_file = make_temp_file();
1457        let path = temp_file.path();
1458
1459        let pidfile = Pidlock::new_validated(path);
1460        assert!(pidfile.is_ok());
1461
1462        let pidfile = pidfile.unwrap();
1463        assert_eq!(pidfile.pid, std::process::id());
1464        assert_eq!(pidfile.path, PathBuf::from(path));
1465        assert_eq!(pidfile.state, PidlockState::New);
1466    }
1467
1468    #[test]
1469    fn test_new_validated_empty_path() {
1470        let result = Pidlock::new_validated("");
1471        match result {
1472            Err(PidlockError::InvalidPath(_)) => {} // Expected
1473            other => panic!("Expected InvalidPath error, got {:?}", other),
1474        }
1475    }
1476
1477    #[test]
1478    fn test_new_validated_problematic_characters() {
1479        // Test various problematic characters
1480        let problematic_paths = [
1481            "/tmp/test<file.pid",
1482            "/tmp/test>file.pid",
1483            "/tmp/test|file.pid",
1484            "/tmp/test?file.pid",
1485            "/tmp/test*file.pid",
1486        ];
1487
1488        for path in &problematic_paths {
1489            let result = Pidlock::new_validated(path);
1490            match result {
1491                Err(PidlockError::InvalidPath(_)) => {} // Expected
1492                other => panic!("Expected InvalidPath for '{}', got {:?}", path, other),
1493            }
1494        }
1495    }
1496
1497    #[test]
1498    #[cfg(target_os = "windows")]
1499    fn test_new_validated_reserved_names_windows() {
1500        let reserved_paths = [
1501            "CON.pid", "PRN.pid", "AUX.pid", "NUL.pid", "COM1.pid", "LPT1.pid",
1502        ];
1503
1504        for path in &reserved_paths {
1505            let result = Pidlock::new_validated(path);
1506            match result {
1507                Err(PidlockError::InvalidPath(_)) => {} // Expected
1508                other => panic!("Expected InvalidPath for '{}', got {:?}", path, other),
1509            }
1510        }
1511    }
1512
1513    #[test]
1514    fn test_error_display_and_chaining() {
1515        use super::InvalidPathError;
1516        use std::error::Error;
1517
1518        // Test InvalidPathError display
1519        let empty_path_err = InvalidPathError::EmptyPath;
1520        assert_eq!(empty_path_err.to_string(), "Path cannot be empty");
1521
1522        let reserved_name_err = InvalidPathError::ReservedName {
1523            filename: "CON.pid".to_string(),
1524        };
1525        assert_eq!(
1526            reserved_name_err.to_string(),
1527            "Filename 'CON.pid' is reserved on Windows"
1528        );
1529
1530        let problematic_char_err = InvalidPathError::ProblematicCharacter {
1531            character: '<',
1532            filename: "test<file.pid".to_string(),
1533        };
1534        assert_eq!(
1535            problematic_char_err.to_string(),
1536            "Filename contains problematic character '<': test<file.pid"
1537        );
1538
1539        // Test PidlockError display
1540        let lock_exists_err = PidlockError::LockExists;
1541        assert_eq!(lock_exists_err.to_string(), "A lock already exists");
1542
1543        let invalid_state_err = PidlockError::InvalidState;
1544        assert_eq!(
1545            invalid_state_err.to_string(),
1546            "An operation was attempted in the wrong state, e.g. releasing before acquiring"
1547        );
1548
1549        // Test error chaining with InvalidPath
1550        let invalid_path_err = PidlockError::InvalidPath(empty_path_err);
1551        assert_eq!(
1552            invalid_path_err.to_string(),
1553            "Invalid path provided for lock file"
1554        );
1555
1556        // Verify error source chain
1557        if let PidlockError::InvalidPath(inner) = &invalid_path_err {
1558            assert_eq!(inner.to_string(), "Path cannot be empty");
1559        }
1560
1561        // Test that std::error::Error trait is implemented
1562        let _: &dyn Error = &invalid_path_err;
1563        let _: &dyn Error = &lock_exists_err;
1564
1565        // Test IO error chaining (create a simple IO error)
1566        let io_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "test error");
1567        let parent_dir_err = InvalidPathError::ParentDirectoryCreationFailed {
1568            path: "/some/path".to_string(),
1569            source: io_error,
1570        };
1571
1572        // Verify error chaining works
1573        assert_eq!(
1574            parent_dir_err.to_string(),
1575            "Cannot create parent directory /some/path"
1576        );
1577        assert!(parent_dir_err.source().is_some());
1578
1579        let pidlock_err = PidlockError::InvalidPath(parent_dir_err);
1580        assert!(pidlock_err.source().is_some());
1581    }
1582
1583    #[test]
1584    fn test_improved_partial_eq() {
1585        use super::InvalidPathError;
1586
1587        // Test that PartialEq works correctly with the new implementation
1588
1589        // Test simple variants
1590        assert_eq!(PidlockError::LockExists, PidlockError::LockExists);
1591        assert_eq!(PidlockError::InvalidState, PidlockError::InvalidState);
1592        assert_ne!(PidlockError::LockExists, PidlockError::InvalidState);
1593
1594        // Test IOError comparison by kind only
1595        let io_err1 = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "message 1");
1596        let io_err2 = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "message 2");
1597        let io_err3 = std::io::Error::new(std::io::ErrorKind::NotFound, "message 3");
1598
1599        let pidlock_io1 = PidlockError::IOError(io_err1);
1600        let pidlock_io2 = PidlockError::IOError(io_err2);
1601        let pidlock_io3 = PidlockError::IOError(io_err3);
1602
1603        // Same error kind should be equal (even with different messages)
1604        assert_eq!(pidlock_io1, pidlock_io2);
1605        // Different error kinds should not be equal
1606        assert_ne!(pidlock_io1, pidlock_io3);
1607
1608        // Test InvalidPath comparison by discriminant
1609        let invalid_path1 = PidlockError::InvalidPath(InvalidPathError::EmptyPath);
1610        let invalid_path2 = PidlockError::InvalidPath(InvalidPathError::EmptyPath);
1611        let invalid_path3 = PidlockError::InvalidPath(InvalidPathError::ReservedName {
1612            filename: "CON.pid".to_string(),
1613        });
1614
1615        // Same discriminant should be equal
1616        assert_eq!(invalid_path1, invalid_path2);
1617        // Different discriminants should not be equal
1618        assert_ne!(invalid_path1, invalid_path3);
1619    }
1620
1621    #[test]
1622    fn test_error_downcasting() {
1623        use super::InvalidPathError;
1624        use std::error::Error;
1625
1626        // Test error downcasting with proper Error trait implementation
1627        let invalid_path_err = PidlockError::InvalidPath(InvalidPathError::EmptyPath);
1628
1629        // Convert to trait object
1630        let err_trait: &dyn Error = &invalid_path_err;
1631
1632        // Test downcasting
1633        assert!(err_trait.downcast_ref::<PidlockError>().is_some());
1634
1635        // Test source chain navigation
1636        if let Some(source) = err_trait.source() {
1637            assert!(source.downcast_ref::<InvalidPathError>().is_some());
1638        }
1639    }
1640
1641    #[test]
1642    fn test_validate_pid_ranges() {
1643        use super::validate_pid;
1644
1645        // Test valid PIDs
1646        assert!(validate_pid(1));
1647        assert!(validate_pid(1000));
1648
1649        // Test invalid PIDs
1650        assert!(!validate_pid(0));
1651        assert!(!validate_pid(-1));
1652        assert!(!validate_pid(-12345));
1653
1654        // Test system-specific upper bounds
1655        #[cfg(target_os = "linux")]
1656        {
1657            // Read the actual maximum PID from /proc/sys/kernel/pid_max, or use default
1658            let max_pid = std::fs::read_to_string("/proc/sys/kernel/pid_max")
1659                .ok()
1660                .and_then(|content| content.trim().parse::<i32>().ok())
1661                .unwrap_or(4194304);
1662
1663            // Test that reasonable PIDs within the max are valid
1664            assert!(validate_pid(1));
1665            assert!(validate_pid(std::cmp::min(max_pid, 1000)));
1666
1667            // Test that max_pid itself is valid
1668            assert!(validate_pid(max_pid));
1669
1670            // Test edge cases around the maximum
1671            if max_pid > 1 {
1672                assert!(validate_pid(max_pid - 1));
1673            }
1674
1675            // Test that max_pid + 1 is invalid (if max_pid < i32::MAX)
1676            if max_pid < i32::MAX {
1677                assert!(!validate_pid(max_pid + 1));
1678            }
1679
1680            // Test that very large values are invalid
1681            assert!(!validate_pid(i32::MAX));
1682
1683            // Ensure the default fallback value (4194304) would be valid
1684            // This helps ensure our fallback is reasonable
1685            assert!(max_pid >= 4194304 || validate_pid(4194304));
1686        }
1687
1688        #[cfg(target_os = "macos")]
1689        {
1690            assert!(validate_pid(99998)); // Should be valid on macOS
1691            assert!(!validate_pid(99999)); // Should be invalid
1692        }
1693    }
1694
1695    #[test]
1696    fn test_get_owner_with_invalid_pid_range() {
1697        let mut temp_file = make_temp_file();
1698        let path = temp_file.path().to_string_lossy().to_string();
1699
1700        // Write a PID that's outside valid range (negative)
1701        temp_file.write_all(b"-500").unwrap();
1702        temp_file.flush().unwrap();
1703
1704        let pidfile = Pidlock::new_validated(&path).unwrap();
1705        let result = pidfile.get_owner().unwrap();
1706        // Invalid PID should be cleaned up
1707        assert_eq!(result, None);
1708        assert!(!std::path::Path::new(&path).exists());
1709    }
1710
1711    #[test]
1712    #[cfg(unix)]
1713    fn test_file_permissions_unix() {
1714        use std::os::unix::fs::PermissionsExt;
1715
1716        let temp_file = make_temp_file();
1717        let path = temp_file.path().to_string_lossy().to_string();
1718
1719        let mut pidfile = Pidlock::new_validated(&path).unwrap();
1720        pidfile.acquire().unwrap();
1721
1722        // Check that file has correct permissions (600 - owner read/write only)
1723        let metadata = std::fs::metadata(&path).unwrap();
1724        let mode = metadata.permissions().mode();
1725        assert_eq!(mode & 0o777, 0o600);
1726
1727        pidfile.release().unwrap();
1728    }
1729
1730    #[test]
1731    #[cfg(unix)]
1732    fn test_file_permissions_security() {
1733        use std::os::unix::fs::PermissionsExt;
1734
1735        let temp_file = make_temp_file();
1736        let path = temp_file.path().to_string_lossy().to_string();
1737
1738        let mut pidfile = Pidlock::new_validated(&path).unwrap();
1739        pidfile.acquire().unwrap();
1740
1741        let metadata = std::fs::metadata(&path).unwrap();
1742        let mode = metadata.permissions().mode();
1743
1744        // Verify the file is NOT readable by group
1745        assert_eq!(mode & 0o040, 0);
1746        // Verify the file is NOT readable by others
1747        assert_eq!(mode & 0o004, 0);
1748        // Verify the file is NOT writable by group
1749        assert_eq!(mode & 0o020, 0);
1750        // Verify the file is NOT writable by others
1751        assert_eq!(mode & 0o002, 0);
1752
1753        // Verify the file IS readable and writable by owner
1754        assert_ne!(mode & 0o400, 0); // Owner read
1755        assert_ne!(mode & 0o200, 0); // Owner write
1756
1757        pidfile.release().unwrap();
1758    }
1759
1760    #[test]
1761    fn test_acquire_detailed_error_handling() {
1762        // Test that we get proper error details instead of generic IOError
1763        // Test the case where we try to create a lock file that already exists
1764        let temp_file = make_temp_file();
1765        let path = temp_file.path().to_string_lossy().to_string();
1766
1767        // First, create and acquire a lock
1768        let mut first_lock = Pidlock::new_validated(&path).unwrap();
1769        first_lock.acquire().unwrap();
1770
1771        // Now try to create a second lock on the same file
1772        let mut second_lock = Pidlock::new_validated(&path).unwrap();
1773        let result = second_lock.acquire();
1774
1775        match result {
1776            Err(PidlockError::LockExists) => {
1777                // This is the expected behavior - proper error type
1778            }
1779            Ok(_) => {
1780                // This shouldn't happen, but if it does, clean up
1781                let _ = second_lock.release();
1782                panic!("Expected LockExists error, but acquire succeeded");
1783            }
1784            Err(other) => {
1785                panic!("Expected LockExists, got {:?}", other);
1786            }
1787        }
1788
1789        // Clean up
1790        first_lock.release().unwrap();
1791    }
1792
1793    #[test]
1794    fn test_process_exists_safety_edge_cases() {
1795        // This test verifies that our safety improvements correctly handle edge cases
1796        // that could previously cause undefined behavior or resource leaks
1797
1798        let mut temp_file = make_temp_file();
1799        let path = temp_file.path().to_string_lossy().to_string();
1800
1801        // Test Case 1: Negative PID that would cause integer overflow on Windows
1802        // This tests our fix for casting i32 to u32 without validation
1803        temp_file.write_all(b"-2147483648").unwrap(); // i32::MIN
1804        temp_file.flush().unwrap();
1805
1806        let pidfile = Pidlock::new_validated(&path).unwrap();
1807        let result = pidfile.get_owner().unwrap();
1808        // Should safely handle the negative PID and clean up the file
1809        assert_eq!(result, None);
1810        assert!(!std::path::Path::new(&path).exists());
1811
1812        // Test Case 2: PID that exceeds platform limits
1813        let mut temp_file = make_temp_file();
1814        let path = temp_file.path().to_string_lossy().to_string();
1815
1816        // Write a PID that exceeds our validate_pid limits
1817        #[cfg(target_os = "linux")]
1818        let invalid_pid = "4194305"; // Just above Linux limit
1819
1820        #[cfg(target_os = "macos")]
1821        let invalid_pid = "100000"; // Just above macOS limit
1822
1823        #[cfg(target_os = "windows")]
1824        let invalid_pid = "4294967296"; // Just above u32::MAX (would overflow)
1825
1826        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
1827        let invalid_pid = "100000"; // Above conservative default
1828
1829        temp_file.write_all(invalid_pid.as_bytes()).unwrap();
1830        temp_file.flush().unwrap();
1831
1832        let pidfile = Pidlock::new_validated(&path).unwrap();
1833        let result = pidfile.get_owner().unwrap();
1834        // Should safely reject the invalid PID and clean up
1835        assert_eq!(result, None);
1836        assert!(!std::path::Path::new(&path).exists());
1837
1838        // Test Case 3: Verify our own PID is correctly detected (positive test)
1839        let mut temp_file = make_temp_file();
1840        let path = temp_file.path().to_string_lossy().to_string();
1841
1842        temp_file
1843            .write_all(std::process::id().to_string().as_bytes())
1844            .unwrap();
1845        temp_file.flush().unwrap();
1846
1847        let pidfile = Pidlock::new_validated(&path).unwrap();
1848        let result = pidfile.get_owner().unwrap();
1849        // Should correctly identify our own process
1850        assert_eq!(result, Some(std::process::id() as i32));
1851
1852        // Clean up
1853        std::fs::remove_file(&path).unwrap();
1854    }
1855
1856    #[test]
1857    fn test_exists() {
1858        let temp_file = make_temp_file();
1859        let pid_path = temp_file.path().to_str().unwrap();
1860
1861        // The temp file exists but we'll test with a different path
1862        let test_path = format!("{}.test", pid_path);
1863        let lock = Pidlock::new_validated(&test_path).unwrap();
1864
1865        assert!(!lock.exists());
1866
1867        // Create the file manually
1868        std::fs::write(&test_path, "1234").unwrap();
1869
1870        // Now it should exist
1871        assert!(lock.exists());
1872
1873        // Clean up
1874        std::fs::remove_file(&test_path).unwrap();
1875        assert!(!lock.exists());
1876    }
1877
1878    #[test]
1879    fn test_is_active() {
1880        let temp_file = make_temp_file();
1881        let pid_path = temp_file.path().to_str().unwrap();
1882        let test_path = format!("{}.test", pid_path);
1883        let lock = Pidlock::new_validated(&test_path).unwrap();
1884
1885        // No lock file should return false
1886        assert!(!lock.is_active().unwrap());
1887
1888        // Create lock file with our own PID
1889        std::fs::write(&test_path, std::process::id().to_string()).unwrap();
1890
1891        // Should be active since our process is running
1892        assert!(lock.is_active().unwrap());
1893
1894        // Create lock file with non-existent PID
1895        std::fs::write(&test_path, "999999").unwrap();
1896
1897        // Should be inactive since PID doesn't exist
1898        assert!(!lock.is_active().unwrap());
1899
1900        // Create lock file with invalid content
1901        std::fs::write(&test_path, "invalid").unwrap();
1902
1903        // get_owner() will clean up invalid files and return Ok(None),
1904        // so is_active() should return Ok(false), not an error
1905        assert!(!lock.is_active().unwrap());
1906
1907        // The invalid file should have been cleaned up
1908        assert!(!lock.exists());
1909    }
1910
1911    #[test]
1912    fn test_check_stale_behavior() {
1913        let temp_file = make_temp_file();
1914        let pid_path = temp_file.path().to_str().unwrap();
1915        let test_path = format!("{}.test", pid_path);
1916        let mut lock = Pidlock::new_validated(&test_path).unwrap();
1917
1918        // No lock file means no stale lock
1919        assert!(!lock.exists());
1920
1921        // Create a stale lock with non-existent PID
1922        std::fs::write(&test_path, "999999").unwrap();
1923
1924        // Acquire should succeed by removing the stale lock
1925        assert!(lock.acquire().is_ok());
1926        assert!(lock.locked());
1927
1928        // Clean up
1929        assert!(lock.release().is_ok());
1930    }
1931
1932    #[test]
1933    fn test_drop_cleanup() {
1934        let temp_file = make_temp_file();
1935        let pid_path = temp_file.path().to_str().unwrap();
1936        let test_path = format!("{}.test", pid_path);
1937
1938        {
1939            let mut lock = Pidlock::new_validated(&test_path).unwrap();
1940            assert!(lock.acquire().is_ok());
1941            assert!(lock.exists());
1942            // Lock goes out of scope here and should be cleaned up
1943        }
1944
1945        // File should be removed by Drop implementation
1946        assert!(!std::path::Path::new(&test_path).exists());
1947    }
1948}