[EXPERIMENTAL] linux apps support
This commit is contained in:
parent
d4996988d9
commit
e7636afe3d
20 changed files with 1377 additions and 101 deletions
|
|
@ -198,6 +198,10 @@ function windowsPathFor(file, prefixDir) {
|
|||
}
|
||||
|
||||
const GRACE_MS = 60000
|
||||
// A finished run may leave output in the pipes (or a grandchild holding them
|
||||
// open): the log closes when its writers are done, at the latest this long
|
||||
// after the exit.
|
||||
const LOG_DRAIN_MS = 5000
|
||||
|
||||
// Live playtime: tiny file updated every 30s, totals mirrored to the registry
|
||||
// on exit. `base` comes from the caller (live file, not the registry) so
|
||||
|
|
@ -222,32 +226,17 @@ function openLatestLog(gameId) {
|
|||
return fs.createWriteStream(logFile, { flags: 'w' })
|
||||
}
|
||||
|
||||
async function runGame({ game, wine, systemWine, onExit, openFile, handoff = false }) {
|
||||
await healPrefixOnce(game)
|
||||
const isProton = wine && wine.type === 'proton'
|
||||
const { env, prefixDir } = buildEnv({ game, wine })
|
||||
const exe = game.exePath
|
||||
const workdir = game.workdir || path.dirname(exe)
|
||||
const argList = parseArgs(game.args)
|
||||
// an opened file is an argument to the exe, exactly like Windows' "Open with"
|
||||
if (openFile) argList.push(windowsPathFor(openFile, prefixDir))
|
||||
|
||||
let cmd
|
||||
let base
|
||||
if (isProton) {
|
||||
cmd = wine.wineBin || path.join(wine.path, 'proton')
|
||||
base = [protonVerb(game), exe, ...argList]
|
||||
} else {
|
||||
cmd = systemWine ? 'wine' : wine.wineBin
|
||||
base = [exe, ...argList]
|
||||
}
|
||||
|
||||
// The tail every tracked run shares (wine and native Linux alike): spawn
|
||||
// detached, tee output into the app's latest log, tick playtime, stamp
|
||||
// lastPlayed, report the exit. `header` lines land right after the launch line.
|
||||
function spawnTracked({ game, cmd, args, cwd, env, handoff = false, header = [], onExit }) {
|
||||
const library = require('./library')
|
||||
fs.mkdirSync(DIRS.DATA, { recursive: true })
|
||||
return new Promise((resolve, reject) => {
|
||||
let child
|
||||
try {
|
||||
child = spawn(cmd, base, {
|
||||
cwd: workdir,
|
||||
child = spawn(cmd, args, {
|
||||
cwd,
|
||||
env,
|
||||
// a handoff is untracked: its pipes drain so the app never blocks
|
||||
stdio: handoff ? 'ignore' : ['ignore', 'pipe', 'pipe'],
|
||||
|
|
@ -272,24 +261,36 @@ async function runGame({ game, wine, systemWine, onExit, openFile, handoff = fal
|
|||
const logFile = latestLogPath(game.id)
|
||||
const ws = openLatestLog(game.id)
|
||||
// a launch line first, so the log answers "which prefix/overrides/run"
|
||||
ws.write(`[plwl] ${cmd} ${base.join(' ')}\n`)
|
||||
ws.write(`[plwl] WINEPREFIX=${env.WINEPREFIX}\n`)
|
||||
if (env.WINEDLLOVERRIDES) ws.write(`[plwl] WINEDLLOVERRIDES=${env.WINEDLLOVERRIDES}\n`)
|
||||
if (isProton) ws.write(`[plwl] proton launcher: ${game.disableUmu ? 'direct wine (no UMU, no Steam)' : `umu.exe (UMU_ID=${env.UMU_ID})`}\n`)
|
||||
child.stdout.pipe(ws)
|
||||
child.stderr.pipe(ws)
|
||||
ws.write(`[plwl] ${cmd} ${args.join(' ')}\n`)
|
||||
for (const line of header) ws.write(line + '\n')
|
||||
// two pipes feed one log, so neither may end it: pipe() would close the
|
||||
// stream on the first EOF, and the second writer then kills the process
|
||||
// with ERR_STREAM_WRITE_AFTER_END (an unhandled WriteStream 'error')
|
||||
child.stdout.pipe(ws, { end: false })
|
||||
child.stderr.pipe(ws, { end: false })
|
||||
let openPipes = 2
|
||||
const endLog = () => {
|
||||
if (--openPipes <= 0) ws.end()
|
||||
}
|
||||
child.stdout.once('end', endLog)
|
||||
child.stderr.once('end', endLog)
|
||||
// an unwritable log (full disk, file removed) must not take us down
|
||||
ws.on('error', () => {})
|
||||
child.unref()
|
||||
const startedAt = Date.now()
|
||||
// read the LIVE playtime file, not the registry (shared ids)
|
||||
const basePlaySeconds = require('./library').readPlaySeconds(game.id) ?? (game.playSeconds || 0)
|
||||
const basePlaySeconds = library.readPlaySeconds(game.id) ?? (game.playSeconds || 0)
|
||||
const stopTicker = startPlaytimeTicker({ game, startedAt, base: basePlaySeconds })
|
||||
// stamp "last played" once at start, even if we crash later
|
||||
require('./library').updateGame(game.id, { lastPlayed: startedAt })
|
||||
library.updateGame(game.id, { lastPlayed: startedAt })
|
||||
child.on('exit', (code, signal) => {
|
||||
stopTicker()
|
||||
ws.end()
|
||||
// nothing may outlive the run: end the log once the buffered output had
|
||||
// its chance, even if something else still holds the pipes
|
||||
const drain = setTimeout(() => ws.end(), LOG_DRAIN_MS)
|
||||
if (drain.unref) drain.unref()
|
||||
const counted = Math.max(0, Math.round((Date.now() - startedAt - GRACE_MS) / 1000))
|
||||
require('./library').updateGame(game.id, {
|
||||
library.updateGame(game.id, {
|
||||
lastPlayed: startedAt,
|
||||
playSeconds: basePlaySeconds + counted
|
||||
})
|
||||
|
|
@ -300,6 +301,32 @@ async function runGame({ game, wine, systemWine, onExit, openFile, handoff = fal
|
|||
})
|
||||
}
|
||||
|
||||
async function runGame({ game, wine, systemWine, onExit, openFile, handoff = false }) {
|
||||
await healPrefixOnce(game)
|
||||
const isProton = wine && wine.type === 'proton'
|
||||
const { env, prefixDir } = buildEnv({ game, wine })
|
||||
const exe = game.exePath
|
||||
const workdir = game.workdir || path.dirname(exe)
|
||||
const argList = parseArgs(game.args)
|
||||
// an opened file is an argument to the exe, exactly like Windows' "Open with"
|
||||
if (openFile) argList.push(windowsPathFor(openFile, prefixDir))
|
||||
|
||||
let cmd
|
||||
let base
|
||||
if (isProton) {
|
||||
cmd = wine.wineBin || path.join(wine.path, 'proton')
|
||||
base = [protonVerb(game), exe, ...argList]
|
||||
} else {
|
||||
cmd = systemWine ? 'wine' : wine.wineBin
|
||||
base = [exe, ...argList]
|
||||
}
|
||||
|
||||
const header = [`[plwl] WINEPREFIX=${env.WINEPREFIX}`]
|
||||
if (env.WINEDLLOVERRIDES) header.push(`[plwl] WINEDLLOVERRIDES=${env.WINEDLLOVERRIDES}`)
|
||||
if (isProton) header.push(`[plwl] proton launcher: ${game.disableUmu ? 'direct wine (no UMU, no Steam)' : `umu.exe (UMU_ID=${env.UMU_ID})`}`)
|
||||
return spawnTracked({ game, cmd, args: base, cwd: workdir, env, handoff, header, onExit })
|
||||
}
|
||||
|
||||
async function runTool({ game, wine, tool, args, systemWine }) {
|
||||
await healPrefixOnce(game)
|
||||
const isProton = wine && wine.type === 'proton'
|
||||
|
|
@ -336,4 +363,4 @@ function runAdHoc({ openPath, wine, systemWine, prefix, onExit }) {
|
|||
return runGame({ game, wine, systemWine, onExit })
|
||||
}
|
||||
|
||||
module.exports = { buildEnv, runGame, runTool, runAdHoc, parseArgs, latestLogPath, windowsPathFor }
|
||||
module.exports = { buildEnv, runGame, runTool, runAdHoc, parseArgs, latestLogPath, windowsPathFor, spawnTracked }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue