'use strict'; const fs = require('fs'); const os = require('os'); const { performance } = require('perf_hooks'); const WAIT_BUFFER = new Int32Array(new SharedArrayBuffer(4)); const DEFAULT_TIMEOUT_MS = 5000; // Cleanup must never replace an arbitrary primary throw (including frozen errors, // primitives, or objects with throwing property traps). Diagnostics are best effort. function attachCleanupError(primary, name, secondary) { if (primary === null || (typeof primary !== 'object' && typeof primary !== 'function')) return; try { Object.defineProperty(primary, name, { value: secondary, configurable: true }); } catch (_error) { /* Preserve the primary when it cannot accept diagnostics. */ } } function hasCode(error, code) { try { return error !== null && error !== undefined && error.code === code; } catch (_error) { return false; } } function sameIdentity(left, right) { return left.ino === right.ino && left.dev === right.dev; } function lostLock(lockPath) { return Object.assign(new Error(`State-store lock changed or lost; refusing to remove it: ${lockPath}`), { code: 'STATE_STORE_LOCK_LOST' }); } function releaseOwnedLock(lockPath, descriptor, identity) { let failed = false; let primary; let pendingDelete = false; let inspected = false; let closeFailed = false; try { const current = fs.lstatSync(lockPath, { bigint: true }); inspected = true; if (!current.isFile() || current.isSymbolicLink() || !sameIdentity(identity, current)) { throw lostLock(lockPath); } // Keep the owned descriptor through validation and unlink. This cooperative // lock is not an atomic compare-and-unlink against a directory mutator. fs.unlinkSync(lockPath); } catch (error) { failed = true; primary = hasCode(error, 'ENOENT') ? lostLock(lockPath) : error; pendingDelete = !inspected && hasCode(error, 'EPERM'); } try { fs.closeSync(descriptor); // Exactly one attempt, even after a release failure. } catch (error) { closeFailed = true; if (failed) attachCleanupError(primary, 'closeError', error); else { failed = true; primary = error; } } if (pendingDelete && !closeFailed) { // Windows can defer deletion until the handle closes. Only a subsequent // ENOENT proves loss; preserve genuine EPERM and never unlink after close. try { fs.lstatSync(lockPath, { bigint: true }); } catch (error) { if (hasCode(error, 'ENOENT')) primary = lostLock(lockPath); } } if (failed) throw primary; } function acquireLock(dbPath, timeoutMs) { const lockPath = `${dbPath}.ecc-state.lock`; const deadline = performance.now() + timeoutMs; for (;;) { let descriptor; try { descriptor = fs.openSync(lockPath, 'wx', 0o600); } catch (error) { if (!hasCode(error, 'EEXIST')) throw error; const remaining = deadline - performance.now(); if (remaining <= 0) { const busy = new Error(`State store is busy: ${dbPath}. Retry after the other ECC operation finishes. ` + `If an operation terminated unexpectedly, stop all ECC processes using this database, ` + `then inspect and remove the leftover lock: ${lockPath}`); busy.code = 'STATE_STORE_BUSY'; throw busy; } Atomics.wait(WAIT_BUFFER, 0, 0, Math.min(20, remaining)); continue; } let identity; try { identity = fs.fstatSync(descriptor, { bigint: true }); fs.writeFileSync(descriptor, `${JSON.stringify({ pid: process.pid, hostname: os.hostname() })}\n`); } catch (error) { try { if (identity) releaseOwnedLock(lockPath, descriptor, identity); else fs.closeSync(descriptor); } catch (releaseError) { attachCleanupError(error, 'releaseError', releaseError); } throw error; } return () => releaseOwnedLock(lockPath, descriptor, identity); } } /** Serialize one synchronous snapshot operation, never a handle's lifetime. * Locks are not expired or stolen: a paused writer must not lose exclusivity. * An abnormal process exit can leave a lock requiring the explicit recovery * described by STATE_STORE_BUSY. No database bytes are changed on timeout. */ function withStateStoreLock(dbPath, callback, { timeoutMs = DEFAULT_TIMEOUT_MS } = {}) { const release = acquireLock(dbPath, timeoutMs); let result; let primaryError; let failed = false; try { result = callback(); } catch (error) { failed = true; primaryError = error; } try { release(); } catch (releaseError) { if (!failed) throw releaseError; attachCleanupError(primaryError, 'releaseError', releaseError); } if (failed) throw primaryError; return result; } module.exports = { withStateStoreLock, attachCleanupError };