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>
This commit is contained in:
68
streamer/windows/lock.ts
Normal file
68
streamer/windows/lock.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* 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 */
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user