// Simple PID-based lock file. Atomic via O_EXCL. // If a stale lock is found (PID no longer alive), it is removed. import { closeSync, openSync, readFileSync, unlinkSync, writeSync } from 'node:fs'; function isAlive(pid) { try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } } export function acquireLock(lockPath) { for (let attempt = 0; attempt < 2; attempt++) { try { const fd = openSync(lockPath, 'wx'); writeSync(fd, String(process.pid)); closeSync(fd); const release = () => { try { unlinkSync(lockPath); } catch { /* ignore */ } }; process.on('exit', release); process.on('SIGINT', () => { release(); process.exit(130); }); process.on('SIGTERM', () => { release(); process.exit(143); }); return release; } catch (err) { if (err.code !== 'EEXIST') throw err; // Lock exists — check if owner is still alive let pid = 0; try { pid = parseInt(readFileSync(lockPath, 'utf8').trim(), 10); } catch { /* unreadable */ } if (pid && isAlive(pid)) { throw new Error(`Cron already running (PID ${pid}, lock ${lockPath})`); } // Stale lock — remove and retry console.log(`Removing stale lock (PID ${pid || '?'} no longer alive): ${lockPath}`); try { unlinkSync(lockPath); } catch { /* ignore */ } } } throw new Error(`Could not acquire lock after retries: ${lockPath}`); }