PLWL/src/ipc/games.js

192 lines
5.9 KiB
JavaScript

'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 }