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}