platform-codebase/features/truth-validation/scripts/lock-manager.ts

123 lines
3 KiB
TypeScript
Executable file

/**
* Lock and queue management for concurrent validation runs
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync, unlinkSync, appendFileSync } from 'fs';
import { join } from 'path';
import type { LockInfo, QueueEntry } from './types.js';
export function ensureRuntimeDir(runtimeDir: string): void {
if (!existsSync(runtimeDir)) {
mkdirSync(runtimeDir, { recursive: true });
}
}
export function isProcessRunning(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
export function isLockHeld(lockFile: string): { held: boolean; info?: LockInfo } {
if (!existsSync(lockFile)) {
return { held: false };
}
try {
const info = JSON.parse(readFileSync(lockFile, 'utf-8')) as LockInfo;
if (isProcessRunning(info.pid)) {
return { held: true, info };
}
// Stale lock - process died
unlinkSync(lockFile);
return { held: false };
} catch {
// Corrupted lock file
try { unlinkSync(lockFile); } catch { /* ignore */ }
return { held: false };
}
}
export function acquireLock(lockFile: string, runtimeDir: string, files: string[]): boolean {
ensureRuntimeDir(runtimeDir);
const lockCheck = isLockHeld(lockFile);
if (lockCheck.held) {
return false;
}
const lockInfo: LockInfo = {
pid: process.pid,
startedAt: new Date().toISOString(),
files,
};
writeFileSync(lockFile, JSON.stringify(lockInfo, null, 2));
// Register cleanup on exit
const cleanup = () => {
try {
if (existsSync(lockFile)) {
const current = JSON.parse(readFileSync(lockFile, 'utf-8')) as LockInfo;
if (current.pid === process.pid) {
unlinkSync(lockFile);
}
}
} catch { /* ignore */ }
};
process.on('exit', cleanup);
process.on('SIGINT', () => { cleanup(); process.exit(130); });
process.on('SIGTERM', () => { cleanup(); process.exit(143); });
return true;
}
export function releaseLock(lockFile: string): void {
try {
if (existsSync(lockFile)) {
const current = JSON.parse(readFileSync(lockFile, 'utf-8')) as LockInfo;
if (current.pid === process.pid) {
unlinkSync(lockFile);
}
}
} catch { /* ignore */ }
}
export function queueValidation(queueFile: string, runtimeDir: string, files: string[], args: QueueEntry['args']): void {
ensureRuntimeDir(runtimeDir);
const entry: QueueEntry = {
files,
queuedAt: new Date().toISOString(),
args,
};
appendFileSync(queueFile, JSON.stringify(entry) + '\n');
}
export function readQueue(queueFile: string): QueueEntry[] {
if (!existsSync(queueFile)) {
return [];
}
try {
const content = readFileSync(queueFile, 'utf-8').trim();
if (!content) return [];
return content.split('\n').map((line) => JSON.parse(line) as QueueEntry);
} catch {
return [];
}
}
export function clearQueue(queueFile: string): void {
try {
if (existsSync(queueFile)) {
unlinkSync(queueFile);
}
} catch { /* ignore */ }
}