'use strict' const { runGame, runTool } = require('../runner') const { importGame, importGameFromPrefix } = require('../importer') const library = require('../library') function killProcessGroup(pid) { try { process.kill(-pid, 'SIGKILL') } catch {} try { process.kill(pid, 'SIGKILL') } catch {} } function sendToWindow(win, channel, data) { if (win && !win.isDestroyed()) win.webContents.send(channel, data) } // Game-process state: id -> { pid, resolved prefix name and wine id }, frozen // at launch (the in-use guards must name what the process really locked). // onGamesChanged fires in the main process when the running set changes. const STARTUP_GRACE_MS = 60000 function createGamesService({ getWindow, onGamesChanged }) { const running = new Map() const startedAt = new Map() const killRequests = new Set() let onGameExit = null const notifyChanged = () => { if (typeof onGamesChanged === 'function') onGamesChanged() } function alive(pid) { if (!pid) return false try { process.kill(pid, 0) return true } catch { return false } } // a stale entry (no pid yet / dead pid) must not answer "already running" function prune() { for (const [id, entry] of [...running]) { if (alive(entry.pid)) continue const age = Date.now() - (startedAt.get(id) || 0) if (!entry.pid && age < STARTUP_GRACE_MS) continue // may still be starting running.delete(id) startedAt.delete(id) } } const isRunning = id => { prune() return running.has(id) } const listRunning = () => { prune() return [...running.keys()] } // snapshot + live name together name every folder a run touches const runningInfo = () => { prune() const out = [] for (const [id, entry] of running) { const game = library.getGame(id) const live = game && game.prefix ? library.prefixNameForGame(game) : '' const prefixes = live && live !== entry.prefix ? [entry.prefix, live] : [entry.prefix] out.push({ id, name: (game && game.name) || id, prefixes, wine: entry.wine }) } return out } // "Open with" handoff: queue the file for the renderer. function openExternally(openPath) { require('../openwith').setPending(openPath) sendToWindow(getWindow(), 'openwith:file', openPath) } function resolveWine(game) { return require('../wines').resolveWine(game.wine || 'system') } function handleExit(id) { running.delete(id) startedAt.delete(id) sendToWindow(getWindow(), 'games:changed') notifyChanged() if (onGameExit) onGameExit(id) } async function start(win, id, openFile) { const game = library.getGame(id) if (!game) return { ok: false, error: 'game-not-found' } prune() let wine try { wine = resolveWine(game) } catch { return { ok: false, error: 'wine-not-found' } } if (running.has(id)) { // a document into a running app is a handoff, not a tracked run if (!openFile) return { ok: false, error: 'already-running' } try { await runGame({ game, wine, systemWine: wine.type === 'system', openFile, handoff: true }) return { ok: true, pid: null, logFile: null } } catch (err) { return { ok: false, error: String((err && err.message) || err) } } } running.set(id, { pid: null, prefix: library.prefixNameForGame(game), wine: wine.id }) startedAt.set(id, Date.now()) try { const res = await runGame({ game, wine, systemWine: wine.type === 'system', onExit: () => handleExit(id), openFile }) // stop requested while still starting: kill and clear if (killRequests.has(id)) { killRequests.delete(id) if (res.pid) killProcessGroup(res.pid) running.delete(id) startedAt.delete(id) sendToWindow(win, 'games:changed') notifyChanged() return { ok: true, pid: null, logFile: res.logFile || null } } // no pid: never keep the entry, it would block the next launch if (!res.pid) { running.delete(id) startedAt.delete(id) return { ok: false, error: 'not-started' } } const entry = running.get(id) if (entry) entry.pid = res.pid sendToWindow(win, 'games:changed') notifyChanged() return { ok: true, pid: res.pid, logFile: res.logFile || null } } catch (err) { running.delete(id) startedAt.delete(id) return { ok: false, error: String((err && err.message) || err) } } } // Stop always clears the state. async function kill(id) { const entry = running.get(id) if (entry) killProcessGroup(entry.pid) else if (running.has(id)) killRequests.add(id) running.delete(id) startedAt.delete(id) sendToWindow(getWindow(), 'games:changed') notifyChanged() return { ok: true } } async function runToolFor(id, tool, args) { const game = library.getGame(id) if (!game) return { ok: false, error: 'game-not-found' } let wine try { wine = resolveWine(game) } catch { return { ok: false, error: 'wine-not-found' } } try { await runTool({ game, wine, tool, args, systemWine: wine.type === 'system' }) return { ok: true } } catch (err) { return { ok: false, error: String((err && err.message) || err) } } } // subscribe to game exits (tray/headless auto-quit) function setOnGameExit(fn) { onGameExit = typeof fn === 'function' ? fn : null } async function importTo(win, srcDir, mode) { return importGame(srcDir, mode, data => sendToWindow(win, 'import:progress', data)) } // prefix import: register in place, nothing copied async function importFromPrefix(win, prefix, srcDir) { return importGameFromPrefix(prefix, srcDir) } return { isRunning, listRunning, runningInfo, start, kill, runToolFor, import: importTo, importFromPrefix, setOnGameExit, openExternally } } module.exports = { createGamesService, killProcessGroup, sendToWindow }