/** * One osu!tourney + tosu stack per PC. tosu reads every osu! process on the * machine and serves the first tourney manager it finds, so two instances * running osu!tourney at once would mix up their scores and rooms. An * instance holds this lock while it runs the stack; others wait and say why. */ import { open, readFile, rm } from 'node:fs/promises'; export interface LockOwner { instance: string; pid: number; at: string; } function alive(pid: number) { try { process.kill(pid, 0); return true; } catch (err) { // EPERM: exists but belongs to someone else. return (err as NodeJS.ErrnoException).code === 'EPERM'; } } export async function lockOwner(path: string): Promise { try { const owner = JSON.parse(await readFile(path, 'utf8')) as LockOwner; return owner && alive(owner.pid) ? owner : null; } catch { return null; } } /** Takes the lock for this process, or returns who holds it. */ export async function acquireLock( path: string, instance: string, pid = process.pid ): Promise<{ ok: true } | { ok: false; owner: LockOwner }> { for (let attempt = 0; attempt < 3; attempt++) { try { const file = await open(path, 'wx'); await file.writeFile(JSON.stringify({ instance, pid, at: new Date().toISOString() })); await file.close(); return { ok: true }; } catch (err) { if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err; } const owner = await lockOwner(path); if (owner && owner.pid === pid) return { ok: true }; if (owner) return { ok: false, owner }; // The holder is gone; clear its lock and try again. await rm(path, { force: true }); } const owner = await lockOwner(path); return owner ? { ok: false, owner } : { ok: false, owner: { instance: 'unknown', pid: 0, at: '' } }; } export async function releaseLock(path: string, pid = process.pid) { try { const owner = JSON.parse(await readFile(path, 'utf8')) as LockOwner; if (owner.pid === pid) await rm(path, { force: true }); } catch { /* not ours or already gone */ } }