Files
Stan Runge 54444a77f0 feat: add native Windows broadcaster and live score modules
- streamer/windows: vash-broadcast.exe (setup/login/start/stop/status/doctor)
  with per-instance OBS, tosu and FFmpeg, a supervised worker, OCR room
  selection and OBS scene reconciliation; built and tested on Linux only
- streamer/relay.ts shared with Vash TV; Cloudflare Access proxy for dev/staging
- worker status validation and live score helpers for the site
- STREAMING_HANDOFF.md and streamer/ALPHA_TEST.md record what is unverified

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 22:02:25 +00:00

69 lines
2.0 KiB
TypeScript

/**
* 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<LockOwner | null> {
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 */
}
}