a LOT sync fixes...
This commit is contained in:
parent
c0f6e802ed
commit
806e173e43
14 changed files with 1073 additions and 240 deletions
139
src/wines.js
139
src/wines.js
|
|
@ -5,6 +5,7 @@ const fsp = fs.promises
|
|||
const path = require('path')
|
||||
const { DIRS, readJson, writeJson, toPortablePath, resolvePortablePath } = require('./paths')
|
||||
const { dedupeDir } = require('./fsutil')
|
||||
const { rewireSymlinks, restoreExecutableBits, EXEC_NAMES } = require('./symlinks')
|
||||
|
||||
const WINES_DIR = DIRS.WINES
|
||||
const REGISTRY = path.join(DIRS.DATA, 'wines.json')
|
||||
|
|
@ -109,12 +110,17 @@ async function refreshWines() {
|
|||
const prev = readJson(REGISTRY, []).map(wineAbs)
|
||||
const prevById = new Map(prev.map(w => [w.id, w]))
|
||||
// A build whose path changed since last scan was moved (or the launcher
|
||||
// itself was) - rewire its absolute symlinks to the current location.
|
||||
// itself was) - rewire its absolute symlinks to the current location. A
|
||||
// build that cannot be executed (no exec bit on proton/wine - exactly what
|
||||
// a sync from a PLWL that predates permission transfer leaves behind) is
|
||||
// healed here instead: two access() calls per build, and only a broken build
|
||||
// pays for the walk. Without it such a build stays dead weight forever.
|
||||
for (const w of found) {
|
||||
const old = prevById.get(w.id)
|
||||
if (!old || old.path !== w.path) {
|
||||
const moved = !old || old.path !== w.path
|
||||
if (moved || buildAuditFailed(w.path, w.wineBin)) {
|
||||
try {
|
||||
await fixBuildSymlinks(w.path)
|
||||
await healBuild(w.path)
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
|
@ -190,77 +196,58 @@ function deleteWine(id) {
|
|||
// that point to absolute paths from wherever the build lived before (zip
|
||||
// layout, previous install dir). After the build or the whole launcher is
|
||||
// moved those links dangle and Proton crashes during prefix setup
|
||||
// (try_copy -> FileNotFoundError). Rewire every absolute symlink whose content
|
||||
// can be located inside the build, always storing the new target as a
|
||||
// *relative* path so the build stays relocatable forever.
|
||||
async function collectSymlinks(dir, out) {
|
||||
let entries
|
||||
try {
|
||||
entries = await fsp.readdir(dir, { withFileTypes: true })
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
for (const e of entries) {
|
||||
const full = path.join(dir, e.name)
|
||||
if (e.isSymbolicLink()) {
|
||||
out.push(full)
|
||||
} else if (e.isDirectory() && e.name !== 'dosdevices') {
|
||||
// dosdevices/ holds prefix drive mappings (c:, z:, ...) which are
|
||||
// *supposed* to point outside the build - never touch them.
|
||||
await collectSymlinks(full, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function rewireTarget(root, link, target) {
|
||||
if (!path.isAbsolute(target)) return null // already relocatable
|
||||
// Absolute link that lives inside the build: make it relative.
|
||||
if (target.startsWith(root + path.sep)) {
|
||||
return path.relative(path.dirname(link), target)
|
||||
}
|
||||
// Absolute link to an old location: find the longest suffix of the target
|
||||
// that exists inside the new build and point there instead.
|
||||
let i = target.indexOf(path.sep, 1)
|
||||
while (i !== -1) {
|
||||
const rel = target.slice(i + 1)
|
||||
if (rel) {
|
||||
const candidate = path.join(root, rel)
|
||||
if (!rel.split(path.sep).includes('..') && candidate.startsWith(root + path.sep) && exists(candidate)) {
|
||||
return path.relative(path.dirname(link), candidate)
|
||||
}
|
||||
}
|
||||
i = target.indexOf(path.sep, i + 1)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// (try_copy -> FileNotFoundError). The rewiring itself lives in symlinks.js
|
||||
// because prefixes, synced apps and game imports need exactly the same thing.
|
||||
async function fixBuildSymlinks(root) {
|
||||
const links = []
|
||||
await collectSymlinks(root, links)
|
||||
let fixed = 0
|
||||
// A link may only become rewirable once another link it chains through has
|
||||
// been fixed, so repeat until a full pass changes nothing.
|
||||
for (let pass = 0; pass < 5; pass++) {
|
||||
let changed = 0
|
||||
for (const link of links) {
|
||||
let target
|
||||
try {
|
||||
target = await fsp.readlink(link)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
const newTarget = rewireTarget(root, link, path.normalize(target))
|
||||
if (!newTarget || newTarget === target) continue
|
||||
try {
|
||||
await fsp.unlink(link)
|
||||
await fsp.symlink(newTarget, link)
|
||||
fixed++
|
||||
changed++
|
||||
} catch {}
|
||||
}
|
||||
if (!changed) break
|
||||
return rewireSymlinks(root, [DIRS.FILES])
|
||||
}
|
||||
|
||||
// Everything a freshly arrived build needs before it can run: relative links
|
||||
// and executable binaries. Returns what was changed, for the callers' reports.
|
||||
async function healBuild(root) {
|
||||
const links = await fixBuildSymlinks(root)
|
||||
const executables = await restoreExecutableBits(root)
|
||||
return { links, executables }
|
||||
}
|
||||
|
||||
function isExec(p) {
|
||||
try {
|
||||
fs.accessSync(p, fs.constants.X_OK)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return fixed
|
||||
}
|
||||
|
||||
// Where a wine/proton build keeps the programs wine itself spawns (wine,
|
||||
// wineserver, wineboot, ...) and the loaders next to the shared libraries.
|
||||
const AUDIT_DIRS = ['files/bin', 'bin', 'dist/bin', 'files/lib/wine/x86_64-unix', 'files/lib/wine/i386-unix']
|
||||
|
||||
// Is this build missing a permission it needs to run? A build synced from a
|
||||
// PLWL that predates permission transfer has no executable bit anywhere, and
|
||||
// that stays invisible until the user presses Play (spawn fails with EACCES).
|
||||
// Auditing is a handful of directory listings - only a build that FAILS it
|
||||
// pays for the full walk, and the audit says nothing about a healthy build on
|
||||
// every later start.
|
||||
function buildAuditFailed(root, wineBin) {
|
||||
if (!root) return false
|
||||
if (wineBin && !isExec(wineBin)) return true
|
||||
for (const rel of AUDIT_DIRS) {
|
||||
const dir = path.join(root, rel)
|
||||
let entries = []
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
const inBin = path.basename(rel) === 'bin'
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue
|
||||
if (!inBin && !EXEC_NAMES.has(entry.name)) continue
|
||||
if (!isExec(path.join(dir, entry.name))) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function importWineFolder(srcDir) {
|
||||
|
|
@ -277,13 +264,13 @@ async function importWineFolder(srcDir) {
|
|||
}
|
||||
const dst = dedupeDir(WINES_DIR, path.basename(copySrc))
|
||||
await fsp.cp(copySrc, dst, { recursive: true, verbatimSymlinks: true })
|
||||
let symlinksFixed = 0
|
||||
let healed = { links: 0, executables: 0 }
|
||||
try {
|
||||
symlinksFixed = await fixBuildSymlinks(dst)
|
||||
healed = await healBuild(dst)
|
||||
} catch {}
|
||||
await refreshWines()
|
||||
// the toast shows a portable-looking path (files/...) like everywhere else
|
||||
return { ok: true, path: toPortablePath(dst), symlinksFixed }
|
||||
return { ok: true, path: toPortablePath(dst), symlinksFixed: healed.links, executablesFixed: healed.executables }
|
||||
}
|
||||
|
||||
module.exports = { refreshWines, getWines, getWine, resolveWine, setWineDisabled, getDefaultWine, setDefaultWine, deleteWine, importWineFolder, findWineBinary, detectType, fixBuildSymlinks }
|
||||
module.exports = { refreshWines, getWines, getWine, resolveWine, setWineDisabled, getDefaultWine, setDefaultWine, deleteWine, importWineFolder, findWineBinary, detectType, fixBuildSymlinks, healBuild }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue