code cleanup

This commit is contained in:
olcxja 2026-10-07 16:29:40 +02:00
commit d21b873327
12 changed files with 40 additions and 213 deletions

View file

@ -42,7 +42,6 @@ function createHashCache(dataDir) {
let cache = null
let dirty = false
let savedAt = 0
const stats = { hits: 0, misses: 0 }
function load() {
if (cache) return cache
@ -80,11 +79,7 @@ function createHashCache(dataDir) {
// the cache dirty only when the timestamp is stale.
function hit(rel, size, mtimeMs) {
const known = load()[rel]
if (!known || known[0] !== size || known[1] !== mtimeMs || typeof known[2] !== 'string') {
stats.misses++
return null
}
stats.hits++
if (!known || known[0] !== size || known[1] !== mtimeMs || typeof known[2] !== 'string') return null
if (Date.now() - (known[3] || 0) > 86400000) {
known[3] = Date.now()
dirty = true
@ -111,35 +106,13 @@ function createHashCache(dataDir) {
cache = null
dirty = false
savedAt = 0
stats.hits = 0
stats.misses = 0
try {
fs.rmSync(FILE, { force: true })
fs.rmSync(`${FILE}.tmp`, { force: true })
} catch {}
}
// Dropping the memory copy makes the next lookup re-read from disk (tests).
function forget() {
cache = null
dirty = false
}
return {
HASH_ALGO,
hashFile,
hit,
remember,
cachedHash,
save,
clear,
forget,
stats: () => ({ ...stats, size: cache ? Object.keys(cache).length : 0, algo: HASH_ALGO }),
resetStats: () => {
stats.hits = 0
stats.misses = 0
}
}
return { HASH_ALGO, hashFile, hit, remember, cachedHash, save, clear }
}
module.exports = { createHashCache, hashFile, HASH_ALGO }

View file

@ -170,7 +170,7 @@ function buildIco(images) {
planes = dims.planes
bpp = dims.bpp
} else {
// legacy fallback: ICONDIRENTRY-style first bytes (PNG-era heuristic)
// nothing measurable: the first bytes carry the size
w = img.readUInt8(0) || 256
h = img.readUInt8(1) || 256
colors = img.readUInt8(2)

View file

@ -8,11 +8,11 @@ function registerLanSyncIpc({ getWindow }) {
ipcMain.handle('lansync:get', () => lansync.getState())
ipcMain.handle('lansync:devices', () => lansync.listDevices())
ipcMain.handle('lansync:rerollDeviceId', () => {
const identity = lansync.rerollDeviceId()
const identity = lansync.rotateDeviceId()
return { deviceId: identity.deviceId }
})
ipcMain.handle('lansync:rerollSyncCode', () => {
const identity = lansync.rerollSyncCode()
const identity = lansync.rotateSyncCode()
return { syncCode: identity.syncCode }
})
ipcMain.handle('lansync:forget', (e, id) => {

View file

@ -93,7 +93,7 @@ function createIdentity(configFile) {
function rememberDevice(id, code, name) {
const cfg = identity()
const stored = String(code || '').trim().toUpperCase()
cfg.known = { ...cfg.known, [id]: { code: stored, syncCode: stored, name: name || '', at: Date.now() } }
cfg.known = { ...cfg.known, [id]: { code: stored, name: name || '', at: Date.now() } }
writeJson(configFile, cfg)
}

View file

@ -44,60 +44,23 @@ const INCOMING_TIMEOUT_MS = Number(process.env.PLWL_LANSYNC_INCOMING_TIMEOUT) ||
// The only trees a sync may touch; the rest of files/ is local state.
const SYNC_ROOTS = ['apps/', 'wines/', 'wineprefixes/', 'data/icons/']
// Sync categories plus lenient aliases for older senders.
// What a sync may carry, one name per category.
const CATEGORIES = ['apps-portable', 'apps-prefix', 'wines', 'prefixes', 'settings']
const CATEGORY_ALIASES = {
// the whole app library: portable + prefix-import launcher entries
apps: ['apps-portable', 'apps-prefix'],
app: ['apps-portable', 'apps-prefix'],
appsportable: ['apps-portable'],
portableapps: ['apps-portable'],
portable: ['apps-portable'],
appsprefix: ['apps-prefix'],
prefixapps: ['apps-prefix'],
appsimportedfromprefix: ['apps-prefix'],
wine: ['wines'],
wines: ['wines'],
wineversions: ['wines'],
wineprefix: ['prefixes'],
wineprefixes: ['prefixes'],
prefixes: ['prefixes'],
prefix: ['prefixes'],
// launcher settings: defaults, wine labels, prefix registry, app configs
settings: ['settings'],
syncsettings: ['settings'],
defaults: ['settings'],
default: ['settings'],
defaultsettings: ['settings']
}
// An app entry's category: portable imports own their apps/ folder.
function appCategory(game) {
if (!game || typeof game !== 'object') return null
if (game.importSource === 'prefix' || game.mode === 'prefix') return 'apps-prefix'
if (game.imported === true || game.mode === 'copy' || game.mode === 'move') return 'apps-portable'
if (game.importSource === 'prefix') return 'apps-prefix'
if (game.imported === true) return 'apps-portable'
return null
}
// An app entry's files, read off its stored path; outside the data = not synced.
function classifyApp(app) {
if (!app || (typeof app !== 'string' && typeof app !== 'object')) return null
const raw = typeof app === 'string' ? app : app.path || app.exePath || app.workdir
// external entry: nothing to sync
if (!raw || typeof raw !== 'string') return 'external'
const value = raw.replace(/\\/g, '/')
const outside = value.startsWith('..') || path.isAbsolute(raw) || /^[a-z]:\//i.test(value) || /^[a-z][a-z0-9+.-]*:\/\//i.test(value)
if (isPrefixRel(value)) return 'prefix'
if (/^apps\//.test(value)) return 'portable'
return outside ? 'external' : null
}
// The list comes from the wire: anything this build does not know is ignored.
function normalizeCategories(list) {
const out = new Set()
for (const raw of Array.isArray(list) ? list : []) {
const aliases = CATEGORY_ALIASES[String(raw || '').toLowerCase().replace(/[^a-z]+/g, '')]
for (const alias of aliases || []) out.add(alias)
const name = String(raw || '')
if (CATEGORIES.includes(name)) out.add(name)
}
return [...out]
}
@ -183,28 +146,6 @@ function targetOf(target) {
return null
}
// Trade the sync code for a token; a rejected code is never stored.
async function verifyPeer(target, code) {
// { host, port, code } in one object, or a target plus a separate code
if (target && typeof target === 'object' && code === undefined && target.code !== undefined) code = target.code
const device = targetOf(target)
if (!device || !device.port) return { ok: false, error: 'bad-target' }
let res
try {
res = await httpJson(device, 'POST', '/plwl/verify', {
json: { deviceId: identity().deviceId, name: os.hostname(), code: String(code == null ? '' : code).trim().toUpperCase() },
timeout: 20000
})
} catch (err) {
return { ok: false, error: String((err && err.message) || err) }
}
if (!res.data || !res.data.ok) return { ok: false, error: (res.data && res.data.error) || `http-${res.status}` }
const peer = res.data.device || { id: res.data.deviceId, name: res.data.name }
const peerId = (target && target.deviceId) || (target && target.id) || (peer && peer.id)
if (peerId) rememberDevice(peerId, code, (peer && peer.name) || (target && target.name))
return { ok: true, token: res.data.token, deviceId: peer && peer.id, device: peer, peer: peer }
}
// --- receiving side ---------------------------------------------------------
function sendJson(res, status, data) {
@ -992,10 +933,8 @@ async function handlePlan(state, session, url, req, res, hooks) {
const PUT_IDLE_MS = Number(process.env.PLWL_LANSYNC_PUT_IDLE) || 60000
async function handlePut(session, url, req, res, hooks) {
// current senders put the pair in the query (percent-encoded); 0.5.x used
// headers, which cannot carry a code above U+00FF at all
const key = req.headers['x-plwl-unit'] || url.searchParams.get('unit') || ''
const rel = req.headers['x-plwl-path'] || url.searchParams.get('path') || ''
const key = url.searchParams.get('unit') || ''
const rel = url.searchParams.get('path') || ''
const unit = session.units.get(key)
const abs = filesPath(rel)
if (!unit || !abs || !unit.paths.has(rel)) {
@ -1066,7 +1005,7 @@ async function handleCommit(session, url, res, hooks) {
async function handleApply(session, req, res, hooks) {
const payload = await readJsonBody(req)
if (!payload) return sendJson(res, 400, { ok: false, error: 'bad-request' })
// sender categories go through the aliases (older PLWL says "defaults")
// only the categories this build knows, whatever the sender sent
const cats = new Set(normalizeCategories(payload.categories))
const appsDeleted = applyApps(payload, cats)
const winesDeleted = cats.has('wines') ? await applyWines(payload) : 0
@ -1091,9 +1030,6 @@ async function handleApply(session, req, res, hooks) {
sendJson(res, 200, {
ok: true,
deleted: removed,
apps: { deleted: appsDeleted },
wines: { deleted: winesDeleted },
prefixes: { deleted: prefixesDeleted },
settings,
defaults: settings ? settings.defaults : null
})
@ -1264,27 +1200,19 @@ const exported = {
startServer,
stop,
stopServer,
closeServer: stopServer,
close: stopServer,
httpJson,
verifyPeer,
runSync,
appCategory,
classifyApp,
CATEGORIES,
normalizeCategories,
serverPort,
getServerPort: serverPort,
getState,
listDevices,
identity,
loadIdentity: identity,
makeDeviceId,
makeSyncCode,
rotateDeviceId,
rotateSyncCode,
rerollDeviceId: rotateDeviceId,
rerollSyncCode: rotateSyncCode,
knownDevice,
knownPeers,
rememberDevice,
@ -1293,9 +1221,6 @@ const exported = {
sameCode,
HASH_ALGO: hashStore.HASH_ALGO,
hashFile,
hashCacheStats: hashStore.stats,
resetHashCacheStats: hashStore.resetStats,
forgetHashCache: hashStore.forget,
clearHashCache: hashStore.clear,
HASH_CONCURRENCY,
progressGate,

View file

@ -283,42 +283,18 @@ function createPusher(api) {
deleted: 0,
bytes: 0,
// the popup reports "3 games, 1 prefix" instead of file counts
items: { apps: 0, wines: 0, prefixes: 0 },
apps: { copied: 0, skipped: 0, deleted: 0 },
wines: { copied: 0, skipped: 0, deleted: 0 },
prefixes: { copied: 0, skipped: 0, deleted: 0 }
items: { apps: 0, wines: 0, prefixes: 0 }
}
}
// Every sync resolves to the same shape, success or failure.
function syncResult(ok, error, summary, events) {
const buckets = { apps: summary.apps, prefixes: summary.prefixes, wines: summary.wines }
summary.progress = events.length
summary.events = events.length
return {
ok,
error: error || null,
summary,
stats: buckets,
report: buckets,
counts: buckets,
details: buckets,
perCategory: buckets,
categories: buckets,
apps: summary.apps,
prefixes: summary.prefixes,
wines: summary.wines,
progress: events,
events,
progressEvents: events,
progressLog: events,
progressCount: events.length
}
function syncResult(ok, error, summary) {
return { ok, error: error || null, summary }
}
// Push to the peer; a rejected code comes back as 'bad-code'.
async function syncToDevice({ deviceId, device: target, code, categories, force, onProgress }) {
const fail = (error, summary, events) => syncResult(false, error, summary || newSummary(0), events || [])
const fail = (error, summary) => syncResult(false, error, summary || newSummary(0))
const device = target || api.listDevices().find(d => d.id === deviceId)
if (!device || !device.address || !device.port) return fail('offline')
const cats = new Set(normalizeCategories(categories))
@ -329,10 +305,7 @@ function createPusher(api) {
const totals = { apps: 0, wines: 0, prefixes: 0, settings: 0 }
for (const unit of units) totals[bucketOf(unit.kind)] += 1
if (cats.has('settings')) totals.settings = 1
// every progress event is kept, so callers can report without subscribing
const events = []
const progress = data => {
events.push(data)
if (onProgress) onProgress(data)
}
@ -435,7 +408,6 @@ function createPusher(api) {
const roots = unit.roots.filter(ownedRoot)
if (!roots.length) continue
const kind = bucketOf(unit.kind)
const bucket = summary[kind]
summary.items[kind] += 1
itemsSeen += 1
barBytes = 0
@ -474,13 +446,9 @@ function createPusher(api) {
if (!plan.data || !plan.data.ok) throw new Error((plan.data && plan.data.error) || `http-${plan.status}`)
const upload = new Set(plan.data.upload || [])
const same = new Set(plan.data.same || [])
// per category: files created/overwritten vs already identical
const fileEntries = entries.filter(e => e.type === 'file')
const filePaths = new Set(fileEntries.map(e => e.path))
const filePaths = new Set(entries.filter(e => e.type === 'file').map(e => e.path))
let sameFiles = 0
for (const p of same) if (filePaths.has(p)) sameFiles++
bucket.copied += fileEntries.length - sameFiles
bucket.skipped += sameFiles
summary.skipped += sameFiles
const pending = []
for (const entry of entries) {
@ -523,10 +491,7 @@ function createPusher(api) {
allDone += pending.length
allTotal += pending.length
const commit = await httpJson(device, 'POST', `/plwl/unit/commit?token=${encodeURIComponent(token)}&unit=${encodeURIComponent(unit.key)}`, { json: {} })
if (commit.data && commit.data.deleted) {
summary.deleted += commit.data.deleted
bucket.deleted += commit.data.deleted
}
if (commit.data && commit.data.deleted) summary.deleted += commit.data.deleted
finished[kind] += 1
// a finished unit leaves a finished bar behind, even one that sent none
barTotal = barTotal || 1
@ -536,10 +501,6 @@ function createPusher(api) {
emit('registry', null, null, null, true)
const applied = await httpJson(device, 'POST', `/plwl/apply?token=${encodeURIComponent(token)}`, { json: buildRegistryPayload([...cats]), timeout: 60000 })
if (!applied.data || !applied.data.ok) throw new Error((applied.data && applied.data.error) || `http-${applied.status}`)
for (const key of ['apps', 'wines', 'prefixes']) {
const deleted = applied.data[key] && applied.data[key].deleted
if (deleted) summary[key].deleted += deleted
}
if (applied.data.deleted) summary.deleted += applied.data.deleted
// a sync with nothing to send still ends with a full bar
barTotal = barTotal || 1
@ -552,11 +513,11 @@ function createPusher(api) {
} catch (err) {
abort.dead = true
await end()
return fail(String((err && err.message) || err), summary, events)
return fail(String((err && err.message) || err), summary)
} finally {
clearInterval(heartbeat)
}
return syncResult(true, null, summary, events)
return syncResult(true, null, summary)
}
// putFile is exposed for the stall tests (it is the piece that must never

View file

@ -109,8 +109,7 @@ function gamesFollowingDefault(running) {
})
}
// Windows is the default: entries written before Linux apps existed have no
// kind at all.
// `kind` is only ever written as 'linux'; a missing one is a Windows app.
function isLinuxGame(game) {
return !!game && game.kind === 'linux'
}

View file

@ -265,13 +265,11 @@ function syncDesktopConfig(root) {
const dst = path.join(root, 'config', rel)
const hostHash = hashOf(src)
if (!hostHash) continue
const known = marker[rel]
if (fs.existsSync(dst) && known) {
if (fs.existsSync(dst)) {
// only a marked copy is provably ours, and only while it is untouched
const current = hashOf(dst)
// the app owns it now, or there is nothing to do
if (current !== known || current === hostHash) continue
if (current !== marker[rel] || current === hostHash) continue
}
// no marker entry: the copy is ours (seeded before the marker existed)
if (copyFromHost(src, dst, { refresh: true })) {
marker[rel] = hostHash
changed = true

View file

@ -24,25 +24,8 @@ function clearPending() {
pending = null
}
// Fall back to English keys on older installs.
function textsFor(code) {
let texts = {}
try {
texts = require('./i18n').getLang(code).texts || {}
} catch {}
const fallback = {
'openwith.title': 'Open with Wine',
'openwith.file': 'File',
'openwith.fileMissing': 'This file no longer exists.',
'openwith.wine': 'Wine / Proton version',
'openwith.wineDefault': 'Default ({0})',
'openwith.prefix': 'Wine prefix',
'openwith.prefixDefault': 'Default prefix ({0})',
'openwith.run': 'Run',
'openwith.startFailed': 'Could not start {0}: {1}',
'openwith.notWindows': 'This is not a Windows executable (exe/msi/bat/cmd/lnk).'
}
return { ...fallback, ...texts }
return require('./i18n').getLang(code).texts || {}
}
function resolveWine(id) {

View file

@ -88,28 +88,16 @@ async function healPrefixOnce(game) {
}
}
// Proton's STEAM_COMPAT_DATA_PATH must be a symlink to the launcher prefix.
// A leftover EMPTY compat dir (an older PLWL bug) is replaced by the link; a
// populated one is left alone.
// Proton's STEAM_COMPAT_DATA_PATH must be a symlink to the launcher prefix;
// a real compat dir already sitting there is not ours to replace.
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
}
if (!fs.lstatSync(compatData).isSymbolicLink()) return
if (fs.readlinkSync(compatData) === prefixDir) return
fs.unlinkSync(compatData)
} catch {}
try {
fs.symlinkSync(prefixDir, compatData, 'dir')