code cleanup
This commit is contained in:
parent
4680d7f014
commit
d21b873327
12 changed files with 40 additions and 213 deletions
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue