Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pidlock"
version = "0.2.0"
version = "0.3.0"
authors = ["Paul Hummer <paul@eventuallyanyway.com>"]
license = "MIT"
edition = "2024"
Expand Down
71 changes: 52 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ A library for creating and managing PID-based file locks, providing a simple and
- **Path validation**: Ensures lock file paths are valid across platforms
- **Safe cleanup**: Automatically releases locks when the `Pidlock` is dropped
- **Comprehensive error handling**: Detailed error types for different failure scenarios
- **Type-safe state management**: Compile-time prevention of invalid state transitions

## Quick Start

Expand All @@ -23,26 +24,29 @@ Add this to your `Cargo.toml`:
pidlock = "0.2"
```

## Basic Usage
## Type-Safe Usage (Recommended)

Starting with version 0.2.0, pidlock uses Rust's type system to prevent invalid state transitions at compile time:

```rust
use pidlock::Pidlock;
use pidlock::{Pidlock, New, Acquired, Released, AcquireError};

fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a new lock
let mut lock = Pidlock::new_validated("/run/lock/my_app.pid")?;
// Create a new lock - starts in New state
let lock: Pidlock<New> = Pidlock::new("/run/lock/my_app.pid")?;

// Try to acquire the lock
// Try to acquire the lock - transitions to Acquired state
match lock.acquire() {
Ok(()) => {
Ok(acquired_lock) => {
println!("Lock acquired successfully!");

// Do your work here...

// Explicitly release the lock (optional - it's auto-released on drop)
lock.release()?;
// Release the lock - transitions to Released state
let _released_lock: Pidlock<Released> = acquired_lock.release()?;
println!("Lock released successfully!");
}
Err(pidlock::PidlockError::LockExists) => {
Err(AcquireError::LockExists) => {
println!("Another instance is already running");
std::process::exit(1);
}
Expand All @@ -56,6 +60,34 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
```

### Compile-Time Safety

The type system prevents common mistakes:

```rust
use pidlock::{Pidlock, New, Acquired, Released};

// This won't compile - cannot acquire from Released state:
// let released_lock: Pidlock<Released> = // ...
// let reacquired = released_lock.acquire(); // ERROR!

// This won't compile - cannot release from New state:
// let lock: Pidlock<New> = // ...
// let released = lock.release(); // ERROR!

// This won't compile - cannot use moved value:
// let lock: Pidlock<New> = // ...
// let acquired = lock.acquire()?;
// println!("{}", lock.exists()); // ERROR! lock was moved
```
std::process::exit(1);
}
}

Ok(())
}
```

## Advanced Usage

### Checking Lock Status
Expand All @@ -64,7 +96,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
use pidlock::Pidlock;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let lock = Pidlock::new_validated("/run/lock/my_app.pid")?;
let lock = Pidlock::new("/run/lock/my_app.pid")?;

// Check if a lock file exists
if lock.exists() {
Expand All @@ -84,13 +116,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
### Error Handling

```rust
use pidlock::{Pidlock, PidlockError, InvalidPathError};
use pidlock::{Pidlock, NewError, InvalidPathError};

fn main() {
let result = Pidlock::new_validated("invalid<path>");
let result = Pidlock::new("invalid<path>");
match result {
Ok(_) => println!("Path is valid"),
Err(PidlockError::InvalidPath(InvalidPathError::ProblematicCharacter { character, filename })) => {
Err(NewError::InvalidPath(InvalidPathError::ProblematicCharacter { character, filename })) => {
println!("Invalid character '{}' in filename: {}", character, filename);
}
Err(e) => println!("Other error: {}", e),
Expand All @@ -105,8 +137,8 @@ use pidlock::Pidlock;

fn main() -> Result<(), Box<dyn std::error::Error>> {
{
let mut lock = Pidlock::new_validated("/run/lock/my_app.pid")?;
lock.acquire()?;
let lock = Pidlock::new("/run/lock/my_app.pid")?;
let acquired_lock = lock.acquire()?;

// Do work here...
// Lock is automatically released when it goes out of scope
Expand All @@ -121,16 +153,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {

If you're upgrading from version 0.1.x:

- Replace `Pidlock::new()` with `Pidlock::new_validated()` for better error handling
- Handle the additional `Result` type returned by `new_validated()`
- Consider the improved error types for more specific error handling
- Replace `Pidlock::new()` with `Pidlock::new()` (no change needed for the method name)
- The API now uses specific error types (`NewError`, `AcquireError`, etc.) instead of a unified `PidlockError`
- Consider the improved type-safe state management for better compile-time safety

```rust
// Old (0.1.x)
let mut lock = pidlock::Pidlock::new("/path/to/pidfile.pid");

// New (0.2.x)
let mut lock = pidlock::Pidlock::new_validated("/path/to/pidfile.pid")?;
let lock = pidlock::Pidlock::new("/path/to/pidfile.pid")?;
let acquired_lock = lock.acquire()?;
```

## Platform Considerations
Expand Down
79 changes: 62 additions & 17 deletions examples/advanced_usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
//! This example demonstrates more sophisticated lock management including
//! error handling, lock status checking, and graceful handling of stale locks.

use pidlock::{InvalidPathError, Pidlock, PidlockError};
use pidlock::{AcquireError, Acquired, InvalidPathError, New, NewError, Pidlock, Released};
use std::env;
use std::process;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
Expand All @@ -18,11 +18,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
"run" => run_with_lock(),
"check" => check_lock_status(),
"cleanup" => cleanup_stale_locks(),
"conversions" => demonstrate_type_conversions(),
_ => {
eprintln!("Usage: {} [run|check|cleanup]", args[0]);
eprintln!(" run - Run the program with lock protection");
eprintln!(" check - Check the status of existing locks");
eprintln!(" cleanup - Clean up any stale lock files");
eprintln!("Usage: {} [run|check|cleanup|conversions]", args[0]);
eprintln!(" run - Run the program with lock protection");
eprintln!(" check - Check the status of existing locks");
eprintln!(" cleanup - Clean up any stale lock files");
eprintln!(" conversions - Demonstrate type conversion patterns");
process::exit(1);
}
}
Expand All @@ -33,13 +35,13 @@ fn run_with_lock() -> Result<(), Box<dyn std::error::Error>> {

println!("Creating lock at: {:?}", lock_path);

let mut lock = match Pidlock::new_validated(&lock_path) {
let lock = match Pidlock::new(&lock_path) {
Ok(lock) => lock,
Err(PidlockError::InvalidPath(InvalidPathError::EmptyPath)) => {
Err(NewError::InvalidPath(InvalidPathError::EmptyPath)) => {
eprintln!("Error: Lock path cannot be empty");
process::exit(1);
}
Err(PidlockError::InvalidPath(InvalidPathError::ProblematicCharacter {
Err(NewError::InvalidPath(InvalidPathError::ProblematicCharacter {
character,
filename,
})) => {
Expand All @@ -49,7 +51,7 @@ fn run_with_lock() -> Result<(), Box<dyn std::error::Error>> {
);
process::exit(1);
}
Err(PidlockError::InvalidPath(InvalidPathError::ReservedName { filename })) => {
Err(NewError::InvalidPath(InvalidPathError::ReservedName { filename })) => {
eprintln!("Error: '{}' is a reserved filename on Windows", filename);
process::exit(1);
}
Expand All @@ -60,18 +62,22 @@ fn run_with_lock() -> Result<(), Box<dyn std::error::Error>> {
};

match lock.acquire() {
Ok(()) => {
Ok(acquired_lock) => {
println!("✓ Lock acquired successfully!");
simulate_work()?;
lock.release()?;
let released_lock = acquired_lock.release()?;
println!("✓ Lock released successfully");
// We could use released_lock here if needed, but dropping it is fine
drop(released_lock);
}
Err(PidlockError::LockExists) => {
Err(AcquireError::LockExists) => {
println!("✗ Lock already exists");
handle_existing_lock(&lock)?;
// Create a new lock instance for inspection since the original was consumed
let inspection_lock = Pidlock::new(&lock_path)?;
handle_existing_lock(&inspection_lock)?;
process::exit(1);
}
Err(PidlockError::IOError(io_err)) => {
Err(AcquireError::IOError(io_err)) => {
eprintln!("✗ I/O error while acquiring lock: {}", io_err);
process::exit(1);
}
Expand All @@ -86,7 +92,7 @@ fn run_with_lock() -> Result<(), Box<dyn std::error::Error>> {

fn check_lock_status() -> Result<(), Box<dyn std::error::Error>> {
let lock_path = get_lock_path()?;
let lock = Pidlock::new_validated(&lock_path)?;
let lock = Pidlock::new(&lock_path)?;

println!("Checking lock status at: {:?}", lock_path);

Expand Down Expand Up @@ -120,7 +126,7 @@ fn check_lock_status() -> Result<(), Box<dyn std::error::Error>> {

fn cleanup_stale_locks() -> Result<(), Box<dyn std::error::Error>> {
let lock_path = get_lock_path()?;
let lock = Pidlock::new_validated(&lock_path)?;
let lock = Pidlock::new(&lock_path)?;

println!("Checking for stale locks at: {:?}", lock_path);

Expand Down Expand Up @@ -148,7 +154,46 @@ fn cleanup_stale_locks() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}

fn handle_existing_lock(lock: &Pidlock) -> Result<(), Box<dyn std::error::Error>> {
fn demonstrate_type_conversions() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Type Conversion Demonstrations ===");

let lock_path = get_lock_path()?;

// Create a new lock - explicit type annotation shows the state
let new_lock: Pidlock<New> = Pidlock::new(&lock_path)?;
println!("✓ Created Pidlock<New>");

// Standard workflow with explicit type annotations
println!("Converting Pidlock<New> -> Pidlock<Acquired> via acquire()...");
let acquired_lock: Pidlock<Acquired> = new_lock.acquire()?;
println!("✓ Conversion successful - now have Pidlock<Acquired>");

// Check that we're the owner
if let Some(owner_pid) = acquired_lock.get_owner()? {
println!(" Lock is owned by PID: {}", owner_pid);
assert_eq!(owner_pid, std::process::id() as i32);
}

println!("Converting Pidlock<Acquired> -> Pidlock<Released> via release()...");
let released_lock: Pidlock<Released> = acquired_lock.release()?;
println!("✓ Conversion successful - now have Pidlock<Released>");

// Verify final state
assert!(!released_lock.exists());
println!("✓ Lock file properly removed during release");

println!("\n=== Type Safety Guarantees ===");
println!("The following operations would cause COMPILE-TIME errors:");
println!(" // released_lock.acquire() - Cannot acquire from Released state");
println!(" // released_lock.release() - Cannot release from Released state");
println!(" // new_lock.release() - Cannot release from New state");
println!(" // new_lock.exists() - Cannot use moved value after acquire()");
println!("✓ Type system prevents invalid state transitions!");

Ok(())
}

fn handle_existing_lock(lock: &Pidlock<New>) -> Result<(), Box<dyn std::error::Error>> {
match lock.get_owner()? {
Some(pid) => {
println!(" Lock is held by PID: {}", pid);
Expand Down
58 changes: 42 additions & 16 deletions examples/basic_usage.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
#![allow(clippy::print_stdout)]
//! Basic usage example for pidlock
//! Basic usage example for pidlock with type-safe state management
//!
//! This example demonstrates the most common use case: ensuring only one instance
//! of a program runs at a time.
//! This example demonstrates the new type-safe API that prevents invalid state
//! transitions at compile time. It also shows the basic workflow and error handling.

use pidlock::{Pidlock, PidlockError};
use pidlock::{AcquireError, New, Pidlock, Released};
use std::env;
use std::process;
use std::thread;
Expand All @@ -29,35 +29,60 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {

println!("Attempting to acquire lock at: {:?}", lock_path);

// Create and try to acquire the lock
let mut lock = Pidlock::new_validated(&lock_path)?;
// Create a new lock - note the type is Pidlock<New>
let lock: Pidlock<New> = Pidlock::new(&lock_path)?;
println!("Lock created in New state");

match lock.acquire() {
Ok(()) => {
Ok(acquired_lock) => {
println!(
"✓ Lock acquired successfully! Process ID: {}",
"✓ Lock acquired successfully! Process ID: {} (Type: Pidlock<Acquired>)",
process::id()
);
println!("This program will run for 10 seconds...");

// We can check that we're the owner
if let Some(owner_pid) = acquired_lock.get_owner()? {
println!("Lock is owned by PID: {}", owner_pid);
assert_eq!(owner_pid, std::process::id() as i32);
}

println!("This program will run for 5 seconds...");

// Simulate some work
for i in 1..=10 {
println!("Working... {}/10", i);
for i in 1..=5 {
println!("Working... {}/5", i);
thread::sleep(Duration::from_secs(1));
}

println!("Work completed! Releasing lock...");
lock.release()?;
println!("✓ Lock released successfully");

// Release the lock - type changes to Pidlock<Released>
let released_lock: Pidlock<Released> = acquired_lock.release()?;
println!("✓ Lock released successfully (Type: Pidlock<Released>)");

// Verify the lock file is gone
assert!(!released_lock.exists());
println!("Lock file has been removed");

// Type Safety Demonstration:
// These would not compile - invalid state transitions:
// let reacquired = released_lock.acquire(); // Compile error! Cannot acquire from Released state
// let re_released = released_lock.release(); // Compile error! Cannot release from Released state
// let bad_release = lock.release(); // Compile error! `lock` was moved in acquire()
// let bad_check = acquired_lock.exists(); // Compile error! `acquired_lock` was moved in release()

println!("✓ Type safety: Invalid operations prevented at compile time!")
}
Err(PidlockError::LockExists) => {
Err(AcquireError::LockExists) => {
println!("✗ Another instance is already running!");

// Try to get information about the existing lock
match lock.get_owner()? {
// Note: we need to create another lock instance to check status
let check_lock: Pidlock<New> = Pidlock::new(&lock_path)?;
match check_lock.get_owner()? {
Some(pid) => {
println!(" Existing lock is held by process ID: {}", pid);
match lock.is_active()? {
match check_lock.is_active()? {
true => println!(" The process is still active"),
false => println!(" The process appears to be dead (stale lock)"),
}
Expand All @@ -73,5 +98,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
}

println!("Example completed successfully!");
Ok(())
}
Loading
Loading