UNPKG

simple-task-master

Version:
371 lines 14.9 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.LockManager = void 0; const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const fs_1 = require("fs"); class LockManager { lockPath; LOCK_TIMEOUT_MS = 30000; // 30 seconds default LOCK_CHECK_INTERVAL_MS = 100; // Retry interval MAX_LOCK_RETRIES = 100; // 10 seconds total wait // Global registry of all LockManager instances to clean up on exit static instances = new Set(); static globalCleanupHandlersSetup = false; static cleanupListeners = null; constructor(projectRoot) { this.lockPath = path.join(projectRoot, '.simple-task-master', 'lock'); // Register this instance for cleanup LockManager.instances.add(this); // Set up global cleanup handlers once if (!LockManager.globalCleanupHandlersSetup) { this.setupGlobalCleanupHandlers(); LockManager.globalCleanupHandlersSetup = true; } } /** * Acquire a lock for exclusive operations. * Uses atomic file creation with O_EXCL flag. * Retries with 100ms intervals up to 50 times (5 seconds total). * Automatically cleans up stale locks. */ async acquire() { const lockData = { pid: process.pid, command: process.argv.join(' '), timestamp: Date.now() }; let retries = 0; while (retries < this.MAX_LOCK_RETRIES) { try { // Check for stale locks before attempting to create if (await this.exists()) { const existingLock = await this.read(); const age = Date.now() - existingLock.timestamp; if (age > this.LOCK_TIMEOUT_MS) { console.warn(`Removing stale lock (age: ${age}ms, pid: ${existingLock.pid})`); await this.forceRelease(); } else if (!this.isProcessAlive(existingLock.pid)) { // Process is dead, but lock is recent - give it a moment to clean up if (age > 500) { console.warn(`Removing stale lock from dead process (age: ${age}ms, pid: ${existingLock.pid})`); await this.forceRelease(); } else { // Lock is fresh but process is dead - wait a bit for cleanup retries++; if (retries >= this.MAX_LOCK_RETRIES) { throw new Error(`Failed to acquire lock after ${this.MAX_LOCK_RETRIES} retries (${this.MAX_LOCK_RETRIES * this.LOCK_CHECK_INTERVAL_MS}ms). ` + 'Another process is holding the lock.'); } await this.sleep(this.LOCK_CHECK_INTERVAL_MS); continue; } } else { // Lock is still valid, wait and retry retries++; if (retries >= this.MAX_LOCK_RETRIES) { throw new Error(`Failed to acquire lock after ${this.MAX_LOCK_RETRIES} retries (${this.MAX_LOCK_RETRIES * this.LOCK_CHECK_INTERVAL_MS}ms). ` + 'Another process is holding the lock.'); } await this.sleep(this.LOCK_CHECK_INTERVAL_MS); continue; } } // Attempt to create lock file atomically with O_EXCL flag const lockContent = JSON.stringify(lockData, null, 2); const fileHandle = await fs.open(this.lockPath, fs_1.constants.O_CREAT | fs_1.constants.O_EXCL | fs_1.constants.O_WRONLY); await fileHandle.write(lockContent); await fileHandle.close(); // Lock acquired successfully return; } catch (error) { const nodeError = error; if (nodeError.code === 'EEXIST') { // Lock file exists, retry after checking if it's stale retries++; if (retries >= this.MAX_LOCK_RETRIES) { const error = new Error(`Failed to acquire lock after ${this.MAX_LOCK_RETRIES} retries (${this.MAX_LOCK_RETRIES * this.LOCK_CHECK_INTERVAL_MS}ms). ` + 'Another process is holding the lock.'); throw error; } await this.sleep(this.LOCK_CHECK_INTERVAL_MS); } else if (nodeError.code === 'ENOENT') { // Directory doesn't exist, create it and retry await fs.mkdir(path.dirname(this.lockPath), { recursive: true }); // Don't increment retries for directory creation } else { // Other errors should be thrown throw error; } } } throw new Error(`Failed to acquire lock after ${this.MAX_LOCK_RETRIES} retries (${this.MAX_LOCK_RETRIES * this.LOCK_CHECK_INTERVAL_MS}ms). ` + 'Another process is holding the lock.'); } /** * Release the lock by removing the lock file. * Only releases if the current process owns the lock. */ async release() { try { const lockData = await this.read(); // Only release if we own the lock if (lockData.pid === process.pid) { await fs.unlink(this.lockPath); } } catch (error) { const nodeError = error; if (nodeError.code !== 'ENOENT') { throw error; } // Lock file doesn't exist, nothing to release } } /** * Check if a lock file exists */ async exists() { try { await fs.access(this.lockPath, fs_1.constants.F_OK); return true; } catch { return false; } } /** * Read and parse the lock file */ async read() { try { const content = await fs.readFile(this.lockPath, 'utf8'); const parsed = JSON.parse(content); // Validate required fields and provide defaults const lockFile = { pid: parsed.pid || 0, command: parsed.command || 'unknown', timestamp: parsed.timestamp || 0 }; // If any critical field is missing or invalid, treat as stale if (!lockFile.pid || !lockFile.timestamp) { return { pid: 99999, // Non-existent PID command: 'stale-corrupted', timestamp: 0 // Very old timestamp }; } return lockFile; } catch { // If we can't parse the file, treat it as stale return { pid: 99999, // Non-existent PID command: 'stale-corrupted', timestamp: 0 // Very old timestamp }; } } /** * Force release a lock without checking ownership */ async forceRelease() { try { await fs.unlink(this.lockPath); } catch (error) { const nodeError = error; if (nodeError.code !== 'ENOENT') { throw error; } } } /** * Check if a process with the given PID is still alive */ isProcessAlive(pid) { try { // Sending signal 0 checks if process exists without actually sending a signal process.kill(pid, 0); return true; } catch (error) { const nodeError = error; // ESRCH means "No such process" if (nodeError.code === 'ESRCH') { return false; } // EPERM means "Operation not permitted" - process exists but we can't signal it if (nodeError.code === 'EPERM') { return true; } // Other errors indicate the process might exist return true; } } /** * Sleep for the specified number of milliseconds */ async sleep(ms) { return new Promise((resolve) => { const timeoutId = global.setTimeout(resolve, ms); // Node.js will keep the event loop alive until the timeout fires if (timeoutId && typeof timeoutId === 'object' && 'unref' in timeoutId) { // Don't prevent process exit if this is the only thing keeping it alive timeoutId.unref(); } }); } /** * Setup global cleanup handlers to release all locks on process exit */ setupGlobalCleanupHandlers() { const cleanup = async () => { // Clean up all registered lock managers const cleanupPromises = Array.from(LockManager.instances).map(async (instance) => { try { await instance.release(); } catch { // Ignore errors during cleanup } }); await Promise.all(cleanupPromises); }; // Check if we're in a test environment const isTestEnvironment = process.env.NODE_ENV === 'test' || process.env.VITEST === 'true' || process.env.JEST_WORKER_ID !== undefined || process.argv.some((arg) => arg.includes('vitest') || arg.includes('jest')); // Create listener functions to store references for cleanup const exitListener = () => { // Synchronous cleanup for exit event try { // Use sync operations for exit for (const instance of LockManager.instances) { try { const lockData = require('fs').readFileSync(instance.lockPath, 'utf8'); const parsed = JSON.parse(lockData); if (parsed.pid === process.pid) { require('fs').unlinkSync(instance.lockPath); } } catch { // Ignore errors during exit cleanup } } } catch { // Ignore errors during exit cleanup } }; const sigintListener = async () => { await cleanup(); if (!isTestEnvironment) { process.exit(0); } }; const sigtermListener = async () => { await cleanup(); if (!isTestEnvironment) { process.exit(0); } }; const uncaughtExceptionListener = async (error) => { console.error('Uncaught exception:', error); await cleanup(); if (!isTestEnvironment) { process.exit(1); } }; const unhandledRejectionListener = async (reason) => { console.error('Unhandled rejection:', reason); await cleanup(); // Don't exit the process in test environments - let test frameworks handle the error if (!isTestEnvironment) { process.exit(1); } }; // Store references for cleanup LockManager.cleanupListeners = { exit: exitListener, sigint: sigintListener, sigterm: sigtermListener, uncaughtException: uncaughtExceptionListener, unhandledRejection: unhandledRejectionListener }; // Handle various exit scenarios process.once('exit', exitListener); process.once('SIGINT', sigintListener); process.once('SIGTERM', sigtermListener); process.once('uncaughtException', uncaughtExceptionListener); process.once('unhandledRejection', unhandledRejectionListener); } /** * Remove this instance from cleanup registry (for testing) */ dispose() { LockManager.instances.delete(this); } /** * Clean up all global event listeners (for testing) */ static disposeGlobalListeners() { if (LockManager.cleanupListeners) { try { process.removeListener('exit', LockManager.cleanupListeners.exit); process.removeListener('SIGINT', LockManager.cleanupListeners.sigint); process.removeListener('SIGTERM', LockManager.cleanupListeners.sigterm); process.removeListener('uncaughtException', LockManager.cleanupListeners.uncaughtException); process.removeListener('unhandledRejection', LockManager.cleanupListeners.unhandledRejection); } catch { // Ignore errors during cleanup } LockManager.cleanupListeners = null; } LockManager.globalCleanupHandlersSetup = false; LockManager.instances.clear(); } } exports.LockManager = LockManager; //# sourceMappingURL=lock-manager.js.map