Part of Tuulbelt — A collection of zero-dependency tools.
Cross-platform file-based semaphore for process synchronization in Node.js.
When multiple processes need to coordinate access to a shared resource (files, ports, build artifacts), you need a cross-platform locking mechanism that:
- Works without external dependencies (no Redis, no database)
- Survives process crashes (stale lock detection)
- Works across different programming languages (compatible file format)
- Zero runtime dependencies - Uses only Node.js built-ins
- Cross-platform - Works on Linux, macOS, and Windows
- CLI and library - Use from command line or import as a module
- Atomic operations - Uses temp file + rename for atomic lock creation
- Stale lock detection - Automatically detects and cleans orphaned locks
- Compatible format - Uses same lock file format as Rust
sematool - 160 tests - Comprehensive test coverage including security tests
git clone https://github.com/tuulbelt/file-based-semaphore-ts.git
cd file-based-semaphore-ts
npm install # Install dev dependencies onlyNo runtime dependencies - this tool uses only Node.js standard library.
CLI names - both short and long forms work:
- Short (recommended):
semats - Long:
file-semaphore-ts
Recommended setup - install globally for easy access:
npm link # Enable the 'semats' command globally
semats --helpFor local development without global install:
npx tsx src/index.ts --help# Try to acquire a lock (non-blocking)
semats try-acquire /tmp/my-lock.lock
# Acquire with optional tag
semats try-acquire /tmp/my-lock.lock --tag "build process"
# Acquire with blocking/timeout
semats acquire /tmp/my-lock.lock --timeout 5000
# Check lock status
semats status /tmp/my-lock.lock
# Release a lock
semats release /tmp/my-lock.lock
# Force release (even if held by another process)
semats release /tmp/my-lock.lock --force
# Clean stale locks and orphaned temp files
semats clean /tmp/my-lock.lock-t, --tag <tag> Tag/description for the lock
--timeout <ms> Timeout in milliseconds (for acquire)
-f, --force Force operation (for release)
-j, --json Output in JSON format
-v, --verbose Verbose output
-h, --help Show help message
# Acquire lock for a build process
semats acquire /tmp/build.lock --tag "npm build" --timeout 30000
npm run build
semats release /tmp/build.lock
# Check if a lock is active
semats status /tmp/my-lock.lock --json
# Output: {"locked":true,"info":{"pid":12345,"timestamp":1234567890,"tag":"my-process"},...}
# Clean up stale locks
semats clean /tmp/my-lock.lockimport { Semaphore } from '@tuulbelt/file-based-semaphore-ts';
// Create a semaphore instance
const semaphore = new Semaphore('/tmp/my-lock.lock');
// Try to acquire (non-blocking)
const result = semaphore.tryAcquire('my-process');
if (result.ok) {
console.log('Lock acquired');
try {
// Do protected work here
} finally {
semaphore.release();
}
} else {
console.log('Lock already held by:', result.error.holderPid);
}
// Acquire with timeout (blocking)
async function withLock() {
const result = await semaphore.acquire({ timeout: 5000 });
if (result.ok) {
try {
// Do protected work
} finally {
semaphore.release();
}
} else {
console.log('Timeout waiting for lock');
}
}
// Check status
const status = semaphore.status();
console.log('Locked:', status.locked);
console.log('Stale:', status.isStale);
console.log('Owned by me:', status.isOwnedByCurrentProcess);
// Clean stale locks and orphaned temp files
const cleaned = semaphore.cleanStale();
console.log('Cleaned:', cleaned);const semaphore = new Semaphore('/tmp/my-lock.lock', {
// Timeout for stale lock detection (ms)
// Set to null to disable stale detection
staleTimeout: 3600000, // 1 hour (default)
// Retry interval when waiting for lock (ms)
retryInterval: 100, // default
// Maximum tag length to prevent resource exhaustion
maxTagLength: 10000, // default
});class Semaphore {
constructor(lockPath: string, config?: SemaphoreConfig);
tryAcquire(tag?: string): SemaphoreResult<LockInfo>;
acquire(options?: { timeout?: number; tag?: string }): Promise<SemaphoreResult<LockInfo>>;
release(force?: boolean): SemaphoreResult<void>;
status(): { locked: boolean; info: LockInfo | null; isStale: boolean; isOwnedByCurrentProcess: boolean };
getLockInfo(): LockInfo | null;
cleanStale(): boolean;
}interface LockInfo {
pid: number; // Process ID holding the lock
timestamp: number; // Unix timestamp when acquired
tag?: string; // Optional description
}type SemaphoreResult<T> =
| { ok: true; value: T }
| { ok: false; error: SemaphoreError };Compatible with the Rust sema tool:
pid=12345
timestamp=1234567890
tag=my-process
- Path traversal prevention - Rejects
..and null bytes in paths - Symlink following - Resolves symlinks to actual target path
- Tag sanitization - Removes all control characters to prevent injection
- PID verification - Only the owning process can release without
--force - Atomic file creation - Uses temp file + rename for race condition mitigation
- Cryptographic randomness - Temp files use random names to prevent DoS
- Restrictive permissions - Lock files created with mode
0600 - Orphan cleanup - Cleans up abandoned temp files
- TOCTOU race condition between checking lock existence and creating it (mitigated but not eliminated by atomic rename)
- Platform-specific behavior for process liveness checks (signal 0 on Unix)
- File permissions depend on filesystem support
npm test # Run all 160 tests
npm run test:unit # Core functionality (52 tests)
npm run test:security # Security tests (26 tests)
npm run test:integration # CLI tests (31 tests)
npm run test:edge # Edge cases (36 tests)
npm run test:stress # Stress tests (15 tests)This tool validates itself using other Tuulbelt tools:
Test Flakiness Detector - Validates test determinism:
./scripts/dogfood-flaky.sh 10
# ✅ NO FLAKINESS DETECTED (160 tests × 10 runs = 1,600 executions)Output Diffing Utility - Proves identical outputs:
./scripts/dogfood-diff.sh
# ✅ IDENTICAL (verified by odiff)Cross-language validation with Rust sema:
./scripts/dogfood-sema.sh
# TypeScript creates lock → Rust reads it ✅
# Rust creates lock → TypeScript reads it ✅See DOGFOODING_STRATEGY.md for details.
- file-based-semaphore - Rust implementation (same lock file format)
- test-port-resolver - Uses this semaphore for port allocation
Part of the Tuulbelt collection.
▶ View interactive recording on asciinema.org
MIT — see LICENSE
