123 lines
3 KiB
TypeScript
Executable file
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 */ }
|
|
}
|