/** * Interlock between the yt-dlp self-update (which REWRITES the managed yt-dlp.exe) * and the download / terminal spawns (which EXECUTE it). On Windows, spawning the * exe while `--update-to` is swapping it in place races into a sharing violation or * launches a half-written binary (L139). This serialises the two: * * - each live yt-dlp process registers via acquireSpawn()/releaseSpawn(); * - the updater calls beginYtdlpUpdate(), which REFUSES (returns false) when any * spawn is live — the update is best-effort and daily-throttled, so it simply * skips this cycle rather than rewrite the exe out from under a running process; * - a spawn that arrives while an update holds the lock is HELD (not failed): the * IPC layer awaits whenYtdlpUpdateSettled() before spawning, so the download just * waits out the brief rewrite instead of erroring. isYtdlpUpdating() is the * synchronous backstop for any caller that doesn't await. * * The main process is single-threaded, so these plain counters/flags need no real * locking: a check-then-act sequence (e.g. await whenYtdlpUpdateSettled() → spawn → * acquireSpawn) runs with no await between the settle and the spawn, so no update can * interleave. Pure (no electron/node chain) so it's unit-testable in isolation. */ let updating = false let liveSpawns = 0 // Resolved by endYtdlpUpdate(); recreated per update so a held spawn wakes exactly // when the current rewrite finishes. Only meaningful while `updating` is true. let settle: (() => void) | null = null let settledPromise: Promise = Promise.resolve() /** True while an update is rewriting yt-dlp.exe — synchronous spawn backstop. */ export function isYtdlpUpdating(): boolean { return updating } /** Register a live yt-dlp process — call immediately after a successful spawn. */ export function acquireSpawn(): void { liveSpawns++ } /** Deregister a yt-dlp process on settle (close/error). Never drops below zero. */ export function releaseSpawn(): void { if (liveSpawns > 0) liveSpawns-- } /** How many yt-dlp processes are currently live (test seam / diagnostics). */ export function liveSpawnCount(): number { return liveSpawns } /** * Resolves once no update is in progress — used to HOLD a spawn until the exe swap * finishes, rather than refuse it. Returns an already-resolved promise when idle, so * the common (non-updating) path adds no delay. Bounded by the caller's update * timeout: endYtdlpUpdate() always runs (finally), so this can't hang forever. */ export function whenYtdlpUpdateSettled(): Promise { return updating ? settledPromise : Promise.resolve() } /** * Enter the update critical section. Returns false — and does NOT take the lock — if * any spawn is live OR another update already holds the lock, in which case the * caller must not update. The updating check matters: a second concurrent begin * (startup auto-update racing a manual "Update now") would otherwise replace the * settle promise, letting the FIRST update's end() release spawns held by the * second one mid-rewrite. On true the caller owns the lock until endYtdlpUpdate(). */ export function beginYtdlpUpdate(): boolean { if (updating || liveSpawns > 0) return false updating = true settledPromise = new Promise((resolve) => { settle = resolve }) return true } /** Leave the update critical section, waking any spawn held on whenYtdlpUpdateSettled(). */ export function endYtdlpUpdate(): void { updating = false settle?.() settle = null }