UNPKG

@socketsecurity/lib

Version:

Core utilities and infrastructure for Socket.dev security tools

294 lines (293 loc) 9.56 kB
"use strict"; /* Socket Lib - Built with esbuild */ var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var process_lock_exports = {}; __export(process_lock_exports, { processLock: () => processLock }); module.exports = __toCommonJS(process_lock_exports); var import_fs = require("fs"); var import_fs2 = require("./fs"); var import_logger = require("./logger"); var import_promises = require("./promises"); var import_signal_exit = require("./signal-exit"); const logger = (0, import_logger.getDefaultLogger)(); class ProcessLockManager { activeLocks = /* @__PURE__ */ new Set(); touchTimers = /* @__PURE__ */ new Map(); exitHandlerRegistered = false; /** * Ensure process exit handler is registered for cleanup. * Registers a handler that cleans up all active locks when the process exits. */ ensureExitHandler() { if (this.exitHandlerRegistered) { return; } (0, import_signal_exit.onExit)(() => { for (const timer of this.touchTimers.values()) { clearInterval(timer); } this.touchTimers.clear(); for (const lockPath of this.activeLocks) { try { if ((0, import_fs.existsSync)(lockPath)) { (0, import_fs2.safeDeleteSync)(lockPath, { recursive: true }); } } catch { } } }); this.exitHandlerRegistered = true; } /** * Touch a lock file to update its mtime. * This prevents the lock from being detected as stale during long operations. * * @param lockPath - Path to the lock directory */ touchLock(lockPath) { try { if ((0, import_fs.existsSync)(lockPath)) { const now = /* @__PURE__ */ new Date(); (0, import_fs.utimesSync)(lockPath, now, now); } } catch (error) { logger.warn( `Failed to touch lock ${lockPath}: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Start periodic touching of a lock file. * Aligned with npm npx strategy to prevent false stale detection. * * @param lockPath - Path to the lock directory * @param intervalMs - Touch interval in milliseconds */ startTouchTimer(lockPath, intervalMs) { if (intervalMs <= 0 || this.touchTimers.has(lockPath)) { return; } const timer = setInterval(() => { this.touchLock(lockPath); }, intervalMs); timer.unref(); this.touchTimers.set(lockPath, timer); } /** * Stop periodic touching of a lock file. * * @param lockPath - Path to the lock directory */ stopTouchTimer(lockPath) { const timer = this.touchTimers.get(lockPath); if (timer) { clearInterval(timer); this.touchTimers.delete(lockPath); } } /** * Check if a lock is stale based on mtime. * Uses second-level granularity to avoid APFS floating-point precision issues. * Aligned with npm's npx locking strategy. * * @param lockPath - Path to the lock directory * @param staleMs - Stale timeout in milliseconds * @returns True if lock exists and is stale */ isStale(lockPath, staleMs) { try { if (!(0, import_fs.existsSync)(lockPath)) { return false; } const stats = (0, import_fs.statSync)(lockPath); const ageSeconds = Math.floor((Date.now() - stats.mtime.getTime()) / 1e3); const staleSeconds = Math.floor(staleMs / 1e3); return ageSeconds > staleSeconds; } catch { return false; } } /** * Acquire a lock using mkdir for atomic operation. * Handles stale locks and includes exit cleanup. * * This method attempts to create a lock directory atomically. If the lock * already exists, it checks if it's stale and removes it before retrying. * Uses exponential backoff with jitter for retry attempts. * * @param lockPath - Path to the lock directory * @param options - Lock acquisition options * @returns Release function to unlock * @throws Error if lock cannot be acquired after all retries * * @example * ```typescript * const release = await processLock.acquire('/tmp/my-lock') * try { * // Critical section * } finally { * release() * } * ``` */ async acquire(lockPath, options = {}) { const { baseDelayMs = 100, maxDelayMs = 1e3, retries = 3, staleMs = 5e3, touchIntervalMs = 2e3 } = options; this.ensureExitHandler(); return await (0, import_promises.pRetry)( async () => { try { if ((0, import_fs.existsSync)(lockPath) && this.isStale(lockPath, staleMs)) { logger.log(`Removing stale lock: ${lockPath}`); try { (0, import_fs2.safeDeleteSync)(lockPath, { recursive: true }); } catch { } } if ((0, import_fs.existsSync)(lockPath)) { throw new Error(`Lock already exists: ${lockPath}`); } (0, import_fs.mkdirSync)(lockPath, { recursive: true }); this.activeLocks.add(lockPath); this.startTouchTimer(lockPath, touchIntervalMs); return () => this.release(lockPath); } catch (error) { const code = error.code; if (code === "EEXIST") { if (this.isStale(lockPath, staleMs)) { throw new Error(`Stale lock detected: ${lockPath}`); } throw new Error(`Lock already exists: ${lockPath}`); } if (code === "EACCES" || code === "EPERM") { throw new Error( `Permission denied creating lock: ${lockPath}. Check directory permissions or run with appropriate access.`, { cause: error } ); } if (code === "EROFS") { throw new Error( `Cannot create lock on read-only filesystem: ${lockPath}`, { cause: error } ); } if (code === "ENOTDIR") { const parentDir = lockPath.slice(0, lockPath.lastIndexOf("/")); throw new Error( `Cannot create lock directory: ${lockPath} A path component is a file when it should be a directory. Parent path: ${parentDir} To resolve: 1. Check if "${parentDir}" contains a file instead of a directory 2. Remove any conflicting files in the path 3. Ensure the full parent directory structure exists`, { cause: error } ); } if (code === "ENOENT") { const parentDir = lockPath.slice(0, lockPath.lastIndexOf("/")); throw new Error( `Cannot create lock directory: ${lockPath} Parent directory does not exist: ${parentDir} To resolve: 1. Ensure the parent directory "${parentDir}" exists 2. Create the directory structure: mkdir -p "${parentDir}" 3. Check filesystem permissions allow directory creation`, { cause: error } ); } throw new Error(`Failed to acquire lock: ${lockPath}`, { cause: error }); } }, { retries, baseDelayMs, maxDelayMs, jitter: true } ); } /** * Release a lock and remove from tracking. * Stops periodic touching and removes the lock directory. * * @param lockPath - Path to the lock directory * * @example * ```typescript * processLock.release('/tmp/my-lock') * ``` */ release(lockPath) { this.stopTouchTimer(lockPath); try { if ((0, import_fs.existsSync)(lockPath)) { (0, import_fs2.safeDeleteSync)(lockPath, { recursive: true }); } this.activeLocks.delete(lockPath); } catch (error) { logger.warn( `Failed to release lock ${lockPath}: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Execute a function with exclusive lock protection. * Automatically handles lock acquisition, execution, and cleanup. * * This is the recommended way to use process locks, as it guarantees * cleanup even if the callback throws an error. * * @param lockPath - Path to the lock directory * @param fn - Function to execute while holding the lock * @param options - Lock acquisition options * @returns Result of the callback function * @throws Error from callback or lock acquisition failure * * @example * ```typescript * const result = await processLock.withLock('/tmp/my-lock', async () => { * // Critical section * return someValue * }) * ``` */ async withLock(lockPath, fn, options) { const release = await this.acquire(lockPath, options); try { return await fn(); } finally { release(); } } } const processLock = new ProcessLockManager(); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { processLock });