a LOT sync fixes...
This commit is contained in:
parent
c0f6e802ed
commit
806e173e43
14 changed files with 1073 additions and 240 deletions
112
src/runner.js
112
src/runner.js
|
|
@ -6,18 +6,66 @@ const { spawn } = require('child_process')
|
|||
const { DIRS } = require('./paths')
|
||||
const { prefixPath } = require('./prefixes')
|
||||
|
||||
// The fake Steam install Proton is pointed at through
|
||||
// STEAM_COMPAT_CLIENT_INSTALL_PATH. Both folders Proton links into its prefix
|
||||
// have to exist: it symlinks <prefix>/drive_c/Program Files (x86)/Steam/
|
||||
// appcache -> <fake>/appcache while setting the prefix up, and a link into a
|
||||
// missing folder is a dangling link in a tree that gets synced.
|
||||
function ensureFakeSteam() {
|
||||
const fake = DIRS.FAKE_STEAM
|
||||
fs.mkdirSync(path.join(fake, 'steamapps'), { recursive: true })
|
||||
fs.mkdirSync(path.join(fake, 'appcache'), { recursive: true })
|
||||
return fake
|
||||
}
|
||||
|
||||
// The prefix a run will use, by NAME (buildEnv resolves the same fallback).
|
||||
function prefixNameFor(game) {
|
||||
if (game && game.prefix) return game.prefix
|
||||
const defs = require('./prefixes').listPrefixes()
|
||||
return defs.default || 'default'
|
||||
}
|
||||
|
||||
// Wine only ever repairs links that are BROKEN: a prefix that came from
|
||||
// another machine (or from an older PLWL) can still hold absolute links into
|
||||
// its build that happen to resolve here, and those would be synced out again.
|
||||
// Healing once per prefix per launch keeps the whole tree relocatable and
|
||||
// costs a single walk in the common (already relative) case.
|
||||
const healedPrefixes = new Set()
|
||||
async function healPrefixOnce(game) {
|
||||
const name = prefixNameFor(game)
|
||||
const key = `${name}\u0000${DIRS.BUILD}`
|
||||
if (!name || healedPrefixes.has(key)) return 0
|
||||
healedPrefixes.add(key)
|
||||
try {
|
||||
return await require('./prefixes').healPrefix(name)
|
||||
} catch {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// Proton gets its prefix through STEAM_COMPAT_DATA_PATH, which has to be a
|
||||
// symlink to the launcher's own prefix folder. The parent directory is created
|
||||
// FIRST: symlink() fails with ENOENT when compat/ does not exist yet, and the
|
||||
// old fallback then created a plain empty directory instead - Proton silently
|
||||
// started with an empty prefix (the game never saw its own data). A leftover
|
||||
// EMPTY directory from exactly that situation is safe to replace with the
|
||||
// link; a populated one is left alone.
|
||||
function linkPrefix(compatData, prefixDir) {
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(compatData), { recursive: true })
|
||||
} catch {}
|
||||
try {
|
||||
const st = fs.lstatSync(compatData)
|
||||
if (st.isSymbolicLink()) {
|
||||
if (fs.readlinkSync(compatData) === prefixDir) return
|
||||
fs.unlinkSync(compatData)
|
||||
} else if (st.isDirectory()) {
|
||||
let names = []
|
||||
try {
|
||||
names = fs.readdirSync(compatData)
|
||||
} catch {}
|
||||
if (names.length) return
|
||||
fs.rmdirSync(compatData)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
|
@ -110,7 +158,8 @@ function openLatestLog(gameId) {
|
|||
return fs.createWriteStream(logFile, { flags: 'w' })
|
||||
}
|
||||
|
||||
function runGame({ game, wine, systemWine, onExit }) {
|
||||
async function runGame({ game, wine, systemWine, onExit }) {
|
||||
await healPrefixOnce(game)
|
||||
const isProton = wine && wine.type === 'proton'
|
||||
const { env } = buildEnv({ game, wine })
|
||||
const exe = game.exePath
|
||||
|
|
@ -140,34 +189,47 @@ function runGame({ game, wine, systemWine, onExit }) {
|
|||
} catch (err) {
|
||||
return reject(err)
|
||||
}
|
||||
const logFile = latestLogPath(game.id)
|
||||
const ws = openLatestLog(game.id)
|
||||
child.stdout.pipe(ws)
|
||||
child.stderr.pipe(ws)
|
||||
child.on('error', reject)
|
||||
child.unref()
|
||||
const startedAt = Date.now()
|
||||
// read the LIVE playtime file, not the (possibly shared/stale) registry
|
||||
// value - ad-hoc "open with" runs share one id and must not mix playtime
|
||||
const basePlaySeconds = require('./library').readPlaySeconds(game.id) ?? (game.playSeconds || 0)
|
||||
const stopTicker = startPlaytimeTicker({ game, startedAt, base: basePlaySeconds })
|
||||
// stamp "last played" once at start so it is correct even if we crash later
|
||||
require('./library').updateGame(game.id, { lastPlayed: startedAt })
|
||||
child.on('exit', (code, signal) => {
|
||||
stopTicker()
|
||||
ws.end()
|
||||
const counted = Math.max(0, Math.round((Date.now() - startedAt - GRACE_MS) / 1000))
|
||||
require('./library').updateGame(game.id, {
|
||||
lastPlayed: startedAt,
|
||||
playSeconds: basePlaySeconds + counted
|
||||
})
|
||||
if (onExit) onExit(code, signal)
|
||||
// spawn() returns before the OS actually starts the program: when the
|
||||
// binary is missing or has no executable bit (a wine build copied without
|
||||
// its permissions, a wrong path) the failure only arrives as an 'error'
|
||||
// event, AFTER this function would have resolved. Resolving there marked
|
||||
// games that never started as running forever, so the promise only settles
|
||||
// once the process really exists ('spawn').
|
||||
let started = false
|
||||
child.once('error', err => {
|
||||
if (!started) reject(err)
|
||||
})
|
||||
child.once('spawn', () => {
|
||||
started = true
|
||||
const logFile = latestLogPath(game.id)
|
||||
const ws = openLatestLog(game.id)
|
||||
child.stdout.pipe(ws)
|
||||
child.stderr.pipe(ws)
|
||||
child.unref()
|
||||
const startedAt = Date.now()
|
||||
// read the LIVE playtime file, not the (possibly shared/stale) registry
|
||||
// value - ad-hoc "open with" runs share one id and must not mix playtime
|
||||
const basePlaySeconds = require('./library').readPlaySeconds(game.id) ?? (game.playSeconds || 0)
|
||||
const stopTicker = startPlaytimeTicker({ game, startedAt, base: basePlaySeconds })
|
||||
// stamp "last played" once at start so it is correct even if we crash later
|
||||
require('./library').updateGame(game.id, { lastPlayed: startedAt })
|
||||
child.on('exit', (code, signal) => {
|
||||
stopTicker()
|
||||
ws.end()
|
||||
const counted = Math.max(0, Math.round((Date.now() - startedAt - GRACE_MS) / 1000))
|
||||
require('./library').updateGame(game.id, {
|
||||
lastPlayed: startedAt,
|
||||
playSeconds: basePlaySeconds + counted
|
||||
})
|
||||
if (onExit) onExit(code, signal)
|
||||
})
|
||||
resolve({ pid: child.pid, logFile })
|
||||
})
|
||||
resolve({ pid: child.pid, logFile })
|
||||
})
|
||||
}
|
||||
|
||||
function runTool({ game, wine, tool, args, systemWine }) {
|
||||
async function runTool({ game, wine, tool, args, systemWine }) {
|
||||
await healPrefixOnce(game)
|
||||
const isProton = wine && wine.type === 'proton'
|
||||
const { env } = buildEnv({ game, wine })
|
||||
const exe = game.exePath
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue