From 1b2c97e74b7877000189dfd6e975907bf17ccb8b Mon Sep 17 00:00:00 2001 From: Devin Bot Date: Sun, 25 Oct 2026 19:27:25 -0700 Subject: [PATCH] potentially fixed auto updater, made sdk also up to date --- main.js | 49 ++++++++++++++++++++++++++++++++++++++---------- package.json | 2 +- patcher.js | 36 +++++++++++++++++++++++++++++++---- ui/index.html | 38 +++++++++++++++++++++++++++++++++++++ update-client.js | 19 +++++++++++++++---- 5 files changed, 125 insertions(+), 19 deletions(-) diff --git a/main.js b/main.js index e4500f3..d72413b 100644 --- a/main.js +++ b/main.js @@ -179,20 +179,37 @@ async function start() { debugLog('Download complete. Verifying checksum...'); sendToTauri('status', { message: 'Verifying SHA-256 integrity...' }); + if (!release.sha256) { + throw new Error('Release has no SHA-256 in manifest.json; refusing unverified install.'); + } if (!patcher.verifyChecksum(tempZipPath, release.sha256)) { throw new Error('SHA-256 checksum verification failed.'); } - debugLog('Checksum verified. Extracting...'); + debugLog('Checksum verified. Extracting to staging...'); sendToTauri('status', { message: 'Unpacking update archive...' }); - patcher.purgeOldCode(); - patcher.extractZipPayload(tempZipPath); + let lastEmit = 0; + const stagingDir = await patcher.extractToStaging(tempZipPath, (file, done, total) => { + const now = Date.now(); + if (done !== total && now - lastEmit < 40) return; + lastEmit = now; + sendToTauri('extract-progress', { + file, + done, + total, + percent: Math.round((done / total) * 100) + }); + }); sendToTauri('status', { message: 'Verifying file integrity...' }); - if (!patcher.verifyExtraction()) { + if (!patcher.verifyExtraction(stagingDir)) { + fs.rmSync(stagingDir, { recursive: true, force: true }); throw new Error('Extraction verification failed: critical startup files missing.'); } + sendToTauri('status', { message: 'Replacing old version...' }); + patcher.commitStaging(stagingDir); + debugLog('Extraction complete. Applying patches...'); sendToTauri('status', { message: 'Applying configuration patches...' }); patcher.applyPatches(getReplacementsConfig()); @@ -224,9 +241,9 @@ async function start() { try { sendToTauri('status', { message: 'Inspecting local workspace and installed files...' }); - let isFirstRun = false; - let localVersion = '0.0.0'; - let localSha = 'force-update-sha'; + const isFirstRun = !patcher.verifyExtraction(); + const localVersion = patcher.getCurrentVersion(); + const localSha = patcher.getCurrentSha(); debugLog(`Is First Run: ${isFirstRun}, Local Version: ${localVersion}, Local SHA: ${localSha}`); function findLocalZip() { @@ -273,11 +290,23 @@ async function start() { } const normalizeVer = (v) => (v || '').toString().replace(/^v/, '').trim(); + const parseVer = (v) => normalizeVer(v).split(/[.\-+]/).map((n) => parseInt(n, 10) || 0); + const isNewer = (remote, local) => { + const a = parseVer(remote); + const b = parseVer(local); + for (let i = 0; i < Math.max(a.length, b.length); i++) { + const x = a[i] || 0; + const y = b[i] || 0; + if (x !== y) return x > y; + } + return false; + }; if (release) { - const isNewerSha = release.sha256 && localSha && release.sha256 !== localSha; - const isDifferentVer = normalizeVer(release.version) !== normalizeVer(localVersion); - const hasUpdate = (isDifferentVer || isNewerSha) && !localZipPath; + const versionNewer = isNewer(release.version, localVersion); + const sameVerNewBuild = normalizeVer(release.version) === normalizeVer(localVersion) && + !!release.sha256 && !!localSha && release.sha256 !== localSha; + const hasUpdate = (versionNewer || sameVerNewBuild) && !localZipPath; if (isFirstRun) { debugLog("First run with no local payload. Installing remote build..."); diff --git a/package.json b/package.json index 5d5767e..1cf2201 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@tauri-apps/plugin-opener": "^2.7.0", "@tauri-apps/plugin-process": "^2.4.0", "@tauri-apps/plugin-shell": "^2.4.0", - "@vyla-entertainment/sdk": "^1.18.0", + "@vyla-entertainment/sdk": "^1.20.0", "adm-zip": "^0.6.0", "express": "^4.19.2" }, diff --git a/patcher.js b/patcher.js index 2ac0591..2ae29a4 100644 --- a/patcher.js +++ b/patcher.js @@ -105,23 +105,51 @@ class Patcher { } } + async extractToStaging(zipPath, onFile) { + const stagingDir = path.join(this.appDataPath, '.staging'); + fs.rmSync(stagingDir, { recursive: true, force: true }); + fs.mkdirSync(stagingDir, { recursive: true }); + const zip = new AdmZip(zipPath); + const entries = zip.getEntries().filter((e) => !e.isDirectory); + if (entries.length === 0) { + throw new Error('Downloaded archive is empty, invalid, or truncated.'); + } + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + zip.extractEntryTo(entry, stagingDir, true, true); + if (onFile) onFile(entry.entryName, i + 1, entries.length); + if (i % 20 === 0) await new Promise((resolve) => setImmediate(resolve)); + } + return stagingDir; + } + + commitStaging(stagingDir) { + this.purgeOldCode(); + for (const name of fs.readdirSync(stagingDir)) { + const dest = path.join(this.appDataPath, name); + fs.rmSync(dest, { recursive: true, force: true }); + fs.renameSync(path.join(stagingDir, name), dest); + } + fs.rmSync(stagingDir, { recursive: true, force: true }); + } + verifyChecksum(zipPath, expectedSha256) { - if (!expectedSha256) return true; + if (!expectedSha256) return false; const hash = crypto.createHash('sha256'); const data = fs.readFileSync(zipPath); hash.update(data); const calculated = hash.digest('hex'); - return calculated === expectedSha256; + return calculated === String(expectedSha256).trim().toLowerCase(); } - verifyExtraction() { + verifyExtraction(root = this.appDataPath) { const requiredFiles = [ 'frontend/server.js', 'player/server.js' ]; for (const file of requiredFiles) { - const fullPath = path.join(this.appDataPath, file); + const fullPath = path.join(root, file); if (!fs.existsSync(fullPath)) { return false; } diff --git a/ui/index.html b/ui/index.html index 8f7e443..0669b87 100644 --- a/ui/index.html +++ b/ui/index.html @@ -87,6 +87,21 @@ } } + .file-line { + width: 100%; + min-height: 16px; + margin-top: -12px; + margin-bottom: 14px; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 11px; + color: rgba(255, 255, 255, 0.45); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + direction: rtl; + text-align: center; + } + .progress-track { width: 100%; height: 10px; @@ -198,6 +213,7 @@ +
@@ -515,6 +531,8 @@ void main() { const actionsEl = document.getElementById("update-actions"); const btnYes = document.getElementById("btn-update-yes"); const btnNo = document.getElementById("btn-update-no"); + const fileLine = document.getElementById("file-line"); + let currentService = ""; if (actionsEl) actionsEl.style.display = "none"; if (progressTrack) progressTrack.style.display = "none"; @@ -562,10 +580,28 @@ void main() { eventApi.listen("status", (e) => { const data = e.payload || e; if (data && data.message) { + if (fileLine) fileLine.textContent = ""; + currentService = ""; setStatus(data.message); } }); + eventApi.listen("extract-progress", (e) => { + const data = e.payload || e; + if (!data || !data.file) return; + if (progressTrack) progressTrack.style.display = "block"; + if (actionsEl) actionsEl.style.display = "none"; + if (progressFill) progressFill.style.width = `${data.percent}%`; + + const parts = String(data.file).split("/"); + const service = parts.length > 1 ? parts[0] : "core files"; + if (service !== currentService) { + currentService = service; + setStatus(`Installing ${service}...`); + } + if (fileLine) fileLine.textContent = `${data.file} (${data.done}/${data.total})`; + }); + eventApi.listen("download-progress", (e) => { const data = e.payload || e; if (data) { @@ -592,6 +628,7 @@ void main() { if (data && data.url) { if (progressTrack) progressTrack.style.display = "none"; if (actionsEl) actionsEl.style.display = "none"; + if (fileLine) fileLine.textContent = ""; setStatus("Launching..."); } }); @@ -599,6 +636,7 @@ void main() { eventApi.listen("error", (e) => { const data = e.payload || e; const msg = data && data.message ? data.message : JSON.stringify(data); + if (fileLine) fileLine.textContent = ""; setStatus("Runtime Error", "#fca5a5"); showDebug(msg); }); diff --git a/update-client.js b/update-client.js index 3cf0253..dca39af 100644 --- a/update-client.js +++ b/update-client.js @@ -36,7 +36,9 @@ class UpdateClient { const urlObj = new URL(url); const headers = { 'User-Agent': 'VylaHome-Client', - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache' }; if (this.token) { @@ -235,12 +237,21 @@ class UpdateClient { } async checkForUpdates() { - const cacheBuster = `?cb=${Date.now()}`; - const releases = await this.fetchJson(RELEASES_API + cacheBuster); + const query = `?per_page=20&order_by=released_at&sort=desc&cb=${Date.now()}`; + const releases = await this.fetchJson(RELEASES_API + query); if (!releases || releases.length === 0) { return null; } - const release = releases[0]; + const verParts = (v) => String(v || '').replace(/^v/, '').split(/[.\-+]/).map((n) => parseInt(n, 10) || 0); + const cmp = (a, b) => { + const x = verParts(a); + const y = verParts(b); + for (let i = 0; i < Math.max(x.length, y.length); i++) { + if ((x[i] || 0) !== (y[i] || 0)) return (x[i] || 0) - (y[i] || 0); + } + return 0; + }; + const release = releases.slice().sort((a, b) => cmp(b.tag_name, a.tag_name))[0]; const payloadAsset = (release.assets && release.assets.links ? release.assets.links : []).find(a => a.name === 'update-payload.zip'); const manifestAsset = (release.assets && release.assets.links ? release.assets.links : []).find(a => a.name === 'manifest.json');