Cross-platform file-based semaphore for process coordination.
When multiple processes need to coordinate access to a shared resource, you need a locking mechanism that:
- Works across different programming languages
- Survives process crashes (with stale lock detection)
- Doesn't require a running daemon or server
- Uses only standard filesystem operations
File-based semaphores provide exactly this: a portable, reliable locking primitive using lock files.
- Zero runtime dependencies (uses only Rust standard library)
- Cross-platform support (Linux, macOS, Windows)
- Both library and CLI interfaces
- Stale lock detection with configurable timeout
- RAII-style guards for automatic release
- Atomic operations using exclusive file creation
git clone https://github.com/tuulbelt/file-based-semaphore.git
cd file-based-semaphore
cargo build --releaseThe binary supports both short and long command names:
- Short (recommended):
target/release/sema - Long:
target/release/file-semaphore
Recommended setup - install globally for easy access:
cargo install --path .
# Now use `sema` anywhere
sema --helpAdd to your Cargo.toml:
[dependencies]
file-based-semaphore = { git = "https://github.com/tuulbelt/file-based-semaphore.git" }# Try to acquire a lock (non-blocking)
sema try /tmp/my.lock
# Acquire with timeout (blocks up to 10 seconds)
sema acquire /tmp/my.lock --timeout 10
# Check lock status
sema status /tmp/my.lock
# Release a lock
sema release /tmp/my.lock
# Wait for a lock to be released
sema wait /tmp/my.lock --timeout 30
# Get JSON output
sema status /tmp/my.lock --jsonuse file_based_semaphore::{Semaphore, SemaphoreConfig};
use std::time::Duration;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let config = SemaphoreConfig {
stale_timeout: Some(Duration::from_secs(60)),
acquire_timeout: Some(Duration::from_secs(5)),
..Default::default()
};
let semaphore = Semaphore::new("/tmp/my-lock.lock", config)?;
// RAII-style: lock auto-released when guard drops
{
let _guard = semaphore.try_acquire()?;
// Critical section - only one process can be here
do_work();
} // Guard dropped, lock released
Ok(())
}Create a new semaphore with the given lock file path.
Try to acquire the lock without blocking. Returns immediately.
Acquire the lock, blocking until available or timeout.
Check if the lock is currently held.
Get information about the current lock holder.
Force release a lock (even if held by another process).
SemaphoreConfig {
stale_timeout: Some(Duration::from_secs(3600)), // Detect stale locks
acquire_timeout: Some(Duration::from_secs(30)), // Max wait time
retry_interval: Duration::from_millis(100), // Polling interval
}Lock files contain simple key-value pairs:
pid=12345
timestamp=1703520000
tag=optional-description
This format is:
- Human-readable for debugging
- Easy to parse from any language
- Forward-compatible (unknown keys are ignored)
| Code | Meaning |
|---|---|
| 0 | Success (lock acquired/released, or lock is free) |
| 1 | Lock already held or timeout |
| 2 | Invalid arguments |
| 3 | IO or system error |
#!/bin/bash
# Acquire lock before running exclusive task
if sema try /tmp/deploy.lock --tag "deploy-$(date +%s)"; then
echo "Lock acquired, deploying..."
./deploy.sh
sema release /tmp/deploy.lock
else
echo "Another deployment in progress"
exit 1
fiuse file_based_semaphore::{Semaphore, SemaphoreConfig};
use std::time::Duration;
// Multiple worker processes competing for exclusive access
let sem = Semaphore::new("/tmp/worker.lock", SemaphoreConfig {
stale_timeout: Some(Duration::from_secs(300)), // 5 min stale timeout
acquire_timeout: Some(Duration::from_secs(60)), // Wait up to 1 min
..Default::default()
})?;
match sem.acquire() {
Ok(guard) => {
process_exclusive_work();
// Guard dropped here, lock released
}
Err(e) => eprintln!("Could not acquire lock: {}", e),
}cargo test # Run all tests
cargo test -- --nocapture # Show outputThis tool demonstrates composability by being VALIDATED BY other Tuulbelt tools:
Test Flakiness Detector - Validate concurrent safety:
./scripts/dogfood-flaky.sh 10
# ✅ NO FLAKINESS DETECTED
# 85 tests × 10 runs = 850 executionsOutput Diffing Utility - Prove deterministic outputs:
./scripts/dogfood-diff.sh
# Test outputs should be identicalThis demonstrates cross-language composition - Rust tools validated by TypeScript tools via CLI interfaces.
Used By: Output Diffing Utility (cache locking demo)
See DOGFOODING_STRATEGY.md for implementation details.
- Portable: Works everywhere with a filesystem
- No daemon: Doesn't require a running service
- Crash-safe: Stale detection handles crashed processes
- Language-agnostic: Any language can read/write lock files
- Platform-specific behavior (especially on NFS)
- Doesn't survive process crashes reliably
- This approach is simpler and more portable
O_CREAT | O_EXCLis atomic on POSIX and Windows- Works on network filesystems
- Clear semantics: file exists = lock held
- Network filesystems: Works but with caveats on NFS (use NFSv4+ with proper locking)
- Race window: Small window between stale detection and acquisition
- Clock skew: Stale detection relies on system time; ensure synchronized clocks
- Test Flakiness Detector - Validates this tool's test suite reliability
- CLI Progress Reporting - Could use this tool for exclusive log access
- Tag injection prevention: Newline characters (
\n,\r) in tags are sanitized to spaces, preventing injection of fake key-value pairs into lock files - Atomic file operations: Uses
O_CREAT | O_EXCLfor atomic lock creation, preventing race conditions - Stale lock detection: Automatically detects and recovers from crashed processes, preventing denial of service
- No privilege escalation: Lock files created with standard user permissions
- Zero runtime dependencies: No supply chain risk from external packages
▶ View interactive recording on asciinema.org
MIT — see LICENSE
