PLWL/src/ipc/games.js

120 lines
3.7 KiB
JavaScript

'use strict'
const { runGame, runTool } = require('../runner')
const { importGame } = 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 (or null while the process is starting).
// onGamesChanged fires in the MAIN process whenever the running-games set
// changes (the tray menu rebuilds itself from it).
function createGamesService({ getWindow, onGamesChanged }) {
const running = new Map()
const killRequests = new Set()
let onGameExit = null
const notifyChanged = () => {
if (typeof onGamesChanged === 'function') onGamesChanged()
}
const isRunning = id => running.has(id)
const listRunning = () => [...running.keys()]
function resolveWine(game) {
const system = { id: 'system', type: 'system', label: 'System Wine' }
const wanted = game.wine === 'default' ? (library.loadSettings().defaultWine || 'system') : (game.wine || 'system')
if (!wanted || wanted === 'system') return system
return require('../wines').getWine(wanted) || system
}
function handleExit(id) {
running.delete(id)
sendToWindow(getWindow(), 'games:changed')
notifyChanged()
if (onGameExit) onGameExit(id)
}
async function start(win, id) {
const game = library.getGame(id)
if (!game) return { ok: false, error: 'game-not-found' }
if (running.has(id)) return { ok: false, error: 'already-running' }
running.set(id, null)
let wine
try {
wine = resolveWine(game)
} catch {
running.delete(id)
return { ok: false, error: 'wine-not-found' }
}
try {
const res = await runGame({ game, wine, systemWine: wine.type === 'system', onExit: () => handleExit(id) })
if (res.pid) running.set(id, res.pid)
// stop was requested while the process was still starting
if (killRequests.has(id)) {
killRequests.delete(id)
if (res.pid) killProcessGroup(res.pid)
}
sendToWindow(win, 'games:changed')
notifyChanged()
return { ok: true, pid: res.pid || null, logFile: res.logFile || null }
} catch (err) {
running.delete(id)
return { ok: false, error: String((err && err.message) || err) }
}
}
async function kill(id) {
const pid = running.get(id)
if (pid) {
killProcessGroup(pid)
running.delete(id)
sendToWindow(getWindow(), 'games:changed')
notifyChanged()
} else if (running.has(id)) {
killRequests.add(id)
}
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 (used by the tray/headless mode to e.g. quit when
// the game that auto-started the launcher has finished)
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))
}
return { isRunning, listRunning, start, kill, runToolFor, import: importTo, setOnGameExit }
}
module.exports = { createGamesService, killProcessGroup, sendToWindow }