Compare commits

..

7 Commits

Author SHA1 Message Date
0c6a9d1e5b v1.1.54 2026-10-30 20:13:21 -07:00
609891f478 updated debug 2026-10-30 20:02:41 -07:00
b201510b5a v1.1.53 2026-10-30 19:37:14 -07:00
acd46d665b fixed image issue 2026-10-30 19:23:15 -07:00
f07c0ea0fd v1.1.52 2026-10-30 19:08:41 -07:00
aaa0df5f17 fixed an extreme ammount of issues 2026-10-30 19:01:30 -07:00
08950e101f v1.1.51 2026-10-30 17:43:06 -07:00
14 changed files with 316 additions and 80 deletions

View File

@@ -24,14 +24,14 @@ Fixing os error 32
Stop-Process -Name "vyla-home", "node", "cargo" -Force -ErrorAction SilentlyContinue
```
Fake a test?
```powershell
$env:VYLA_TEST_UI="1"; npm run dev
```
Fake an old local version:
```powershell
'{"version":"0.0.1","sha256":null}' | Set-Content "$env:APPDATA\vyla-home\vyla-home-files\version.json"
```
Fake a test?
```powershell
$env:VYLA_TEST_UI="1"; npm run dev
```

View File

@@ -174,7 +174,7 @@ function validatePayload(zipPath) {
const requiredFiles = [
'frontend/index.html'
];
const validationResults = {
valid: true,
errors: [],

View File

@@ -9,6 +9,9 @@ const SDK_NAME = '@vyla-entertainment/sdk';
const SDK_LATEST_URL = 'https://registry.npmjs.org/@vyla-entertainment%2Fsdk/latest';
const ALLOWED_ROOTS = ['frontend', 'player'];
const MAX_TARBALL = 100 * 1024 * 1024;
const MAX_FILE = 50 * 1024 * 1024;
const LIVE_HOSTS = ['gitlab.com'];
const REPO_API = 'https://gitlab.com/api/v4/projects/vyla-entertainment%2F';
const verParts = (v) => String(v || '').replace(/^v/, '').split(/[.\-+]/).map((n) => parseInt(n, 10) || 0);
const cmpVer = (a, b) => {
@@ -22,8 +25,11 @@ const cmpVer = (a, b) => {
const sha256Buf = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
const sha256File = (p) => sha256Buf(fs.readFileSync(p));
const gitBlobSha = (buf) => crypto.createHash('sha1').update(Buffer.concat([Buffer.from(`blob ${buf.length}\0`), buf])).digest('hex');
function fetchBuffer(url, { headers = {}, maxBytes, hosts, redirects = 0 }) {
const hooks = { notice: null };
function fetchBuffer(url, { headers = {}, maxBytes, hosts, redirects = 0, retries = 0 }) {
return new Promise((resolve, reject) => {
if (redirects > 5) return reject(new Error('Too many redirects'));
let u;
@@ -44,6 +50,15 @@ function fetchBuffer(url, { headers = {}, maxBytes, hosts, redirects = 0 }) {
fetchBuffer(next, { headers: keep, maxBytes, hosts, redirects: redirects + 1 }).then(resolve, reject);
return;
}
if (res.statusCode === 429 && retries < 4) {
res.resume();
const wait = Math.min(parseInt(res.headers['retry-after'], 10) || 5, 30) * 1000;
if (hooks.notice) hooks.notice(`GitLab rate limit (HTTP 429). Retrying in ${Math.round(wait / 1000)}s (${retries + 1}/4)...`);
setTimeout(() => {
fetchBuffer(url, { headers, maxBytes, hosts, redirects, retries: retries + 1 }).then(resolve, reject);
}, wait);
return;
}
if (res.statusCode !== 200) {
res.resume();
reject(new Error(`HTTP ${res.statusCode}`));
@@ -114,16 +129,24 @@ class FileUpdater {
this.appDataPath = appDataPath;
this.updateClient = updateClient;
this.onProgress = null;
this.pending = null;
this.statePath = path.join(appDataPath, 'file-state.json');
}
set onNotice(fn) {
hooks.notice = fn;
}
sdkHeaders() {
const h = { Accept: 'application/json' };
if (process.env.NPM_TOKEN) h.Authorization = `Bearer ${process.env.NPM_TOKEN}`;
return h;
}
liveHeaders(extra = {}) {
const t = this.updateClient && this.updateClient.token;
return t ? { ...extra, 'PRIVATE-TOKEN': t } : extra;
}
target(rel) {
const root = path.resolve(this.appDataPath);
const full = path.resolve(root, ...rel.split('/'));
@@ -136,17 +159,28 @@ class FileUpdater {
const s = JSON.parse(fs.readFileSync(this.statePath, 'utf8'));
return {
seq: typeof s.seq === 'number' ? s.seq : null,
base: s.base && typeof s.base === 'object' ? s.base : {}
base: s.base && typeof s.base === 'object' ? s.base : {},
remote: s.remote && typeof s.remote === 'object' ? s.remote : {},
local: s.local && typeof s.local === 'object' ? s.local : {},
commits: s.commits && typeof s.commits === 'object' ? s.commits : {}
};
} catch (e) {
return { seq: null, base: {} };
return { seq: null, base: {}, remote: {}, local: {}, commits: {} };
}
}
writeState(state) {
const cur = this.readState();
const next = {
seq: state.seq !== undefined ? state.seq : cur.seq,
base: state.base || cur.base,
remote: state.remote || cur.remote,
local: state.local || cur.local,
commits: state.commits || cur.commits
};
fs.mkdirSync(this.appDataPath, { recursive: true });
const tmp = `${this.statePath}.tmp`;
fs.writeFileSync(tmp, JSON.stringify(state), 'utf8');
fs.writeFileSync(tmp, JSON.stringify(next), 'utf8');
fs.renameSync(tmp, this.statePath);
}
@@ -166,7 +200,7 @@ class FileUpdater {
const dir = path.join(this.appDataPath, root);
if (fs.existsSync(dir)) walk(dir, root);
}
this.writeState({ seq: this.readState().seq, base });
this.writeState({ seq: this.readState().seq, base, remote: {}, local: {}, commits: {} });
}
zipMatches(zipPath, sha256) {
@@ -296,6 +330,213 @@ class FileUpdater {
return changed ? Buffer.from(text, 'utf8') : buf;
}
async repoTree(root) {
const out = [];
for (let page = 1; page <= 50; page++) {
const url = `${REPO_API}${root}/repository/tree?recursive=true&per_page=100&ref=main&page=${page}`;
return fetchBuffer(`${REPO_API}${root}/repository/files/${file}/raw?ref=main`, { headers: this.liveHeaders(), maxBytes: MAX_FILE, hosts: LIVE_HOSTS });
const items = JSON.parse(raw.toString('utf8'));
if (!Array.isArray(items) || items.length === 0) break;
out.push(...items);
if (items.length < 100) break;
}
return out;
}
async loadArchive(root) {
const buf = await fetchBuffer(`${REPO_API}${root}/repository/archive.zip?sha=main`, { headers: this.liveHeaders(), maxBytes: 600 * 1024 * 1024, hosts: LIVE_HOSTS });
const zip = new AdmZip(buf);
const map = new Map();
for (const entry of zip.getEntries()) {
if (entry.isDirectory) continue;
const name = entry.entryName;
const i = name.indexOf('/');
if (i < 0) continue;
map.set(`${root}/${name.slice(i + 1)}`, entry);
}
return map;
}
fetchRepoFile(rel) {
const hit = this.archive && this.archive.get(rel);
if (hit) return Promise.resolve(hit.getData());
const parts = rel.split('/');
const root = parts.shift();
const file = encodeURIComponent(parts.join('/'));
return fetchBuffer(`${REPO_API}${root}/repository/files/${file}/raw?ref=main`, { maxBytes: MAX_FILE, hosts: LIVE_HOSTS });
}
async headCommit(root) {
try {
const raw = await fetchBuffer(`${REPO_API}${root}/repository/commits/main`, {
headers: this.liveHeaders({ Accept: 'application/json' }),
maxBytes: 1024 * 1024,
hosts: LIVE_HOSTS,
retries: 3
});
const id = JSON.parse(raw.toString('utf8')).id;
return /^[0-9a-f]{40}$/.test(id) ? id : null;
} catch (e) {
return null;
}
}
localIntact(root, state) {
const prefix = `${root}/`;
const keys = Object.keys(state.remote).filter((k) => k.startsWith(prefix));
if (keys.length === 0) return false;
for (const rel of keys) {
try {
if (sha256File(this.target(rel)) !== state.local[rel]) return false;
} catch (e) {
return false;
}
}
return true;
}
async checkRepos() {
const state = this.readState();
const changes = [];
const remoteIds = {};
const checkedRoots = [];
const errors = [];
const verified = { remote: {}, local: {} };
const commits = {};
this.archive = new Map();
for (const root of ALLOWED_ROOTS) {
const head = await this.headCommit(root);
if (this.localIntact(root, state) && (!head || head === state.commits[root])) continue;
let map;
try {
map = await this.loadArchive(root);
} catch (e) {
errors.push(`${root}: ${e.message}`);
continue;
}
if (map.size === 0) {
errors.push(`${root}: empty archive`);
continue;
}
checkedRoots.push(root);
if (head) commits[root] = head;
for (const [key, entry] of map) {
const rel = safeRel(key);
if (!rel) continue;
const data = entry.getData();
const id = gitBlobSha(data);
remoteIds[rel] = id;
const full = this.target(rel);
let ok = false;
let localHash = null;
if (fs.existsSync(full)) {
const buf = fs.readFileSync(full);
localHash = sha256Buf(buf);
ok = gitBlobSha(buf) === id || (state.remote[rel] === id && state.local[rel] === localHash);
}
if (ok) {
verified.remote[rel] = id;
verified.local[rel] = localHash;
} else {
changes.push({ path: rel, sha256: id, size: data.length });
this.archive.set(rel, entry);
}
}
}
if (checkedRoots.length === 0) {
if (errors.length) throw new Error(errors.join(' | '));
return { seq: state.seq === null ? 0 : state.seq, changes: [], removed: [], verified, commits };
}
const tracked = new Set([...Object.keys(state.base), ...Object.keys(state.remote)]);
const removed = [...tracked].filter((rel) =>
checkedRoots.includes(rel.split('/')[0]) &&
!remoteIds[rel] &&
safeRel(rel) &&
fs.existsSync(this.target(rel))
);
if (changes.length === 0 && removed.length === 0) {
for (const k of Object.keys(verified.remote)) {
state.remote[k] = verified.remote[k];
state.local[k] = verified.local[k];
}
this.writeState({ remote: state.remote, local: state.local, commits: { ...state.commits, ...commits } });
this.archive = null;
}
return { seq: state.seq === null ? 0 : state.seq, changes, removed, verified, commits };
}
async applyLive(info, replacements, onFile) {
const staged = [];
const total = info.changes.length;
let next = 0;
if (!this.archive) this.archive = new Map();
let finished = 0;
let failure = null;
const worker = async () => {
while (next < total && !failure) {
const c = info.changes[next++];
try {
const data = await this.fetchRepoFile(c.path);
if (gitBlobSha(data) !== c.sha256) throw new Error(`Checksum mismatch: ${c.path}`);
staged.push({ c, data: this.applyReplacements(c.path, data, replacements) });
finished++;
if (onFile) onFile(c.path, finished, total);
} catch (e) {
failure = e;
}
}
};
await Promise.all(Array.from({ length: Math.min(4, total) }, worker));
this.archive = null;
if (failure) throw failure;
const done = [];
try {
for (const s of staged) {
const full = this.target(s.c.path);
const prev = fs.existsSync(full) ? fs.readFileSync(full) : null;
fs.mkdirSync(path.dirname(full), { recursive: true });
fs.writeFileSync(`${full}.vyla-tmp`, s.data);
fs.renameSync(`${full}.vyla-tmp`, full);
done.push({ full, prev });
}
for (const rel of info.removed) {
const full = this.target(rel);
if (!fs.existsSync(full)) continue;
const prev = fs.readFileSync(full);
fs.rmSync(full, { force: true });
done.push({ full, prev });
}
} catch (e) {
for (const d of done.reverse()) {
try {
if (d.prev === null) fs.rmSync(d.full, { force: true });
else fs.writeFileSync(d.full, d.prev);
} catch (x) { }
}
throw e;
}
const state = this.readState();
for (const s of staged) {
state.base[s.c.path] = s.c.sha256;
state.remote[s.c.path] = s.c.sha256;
state.local[s.c.path] = sha256Buf(s.data);
}
for (const rel of info.removed) {
delete state.base[rel];
delete state.remote[rel];
delete state.local[rel];
}
const v = info.verified || { remote: {}, local: {} };
for (const k of Object.keys(v.remote)) {
state.remote[k] = v.remote[k];
state.local[k] = v.local[k];
}
state.commits = { ...state.commits, ...(info.commits || {}) };
this.writeState({ seq: info.seq, base: state.base, remote: state.remote, local: state.local, commits: state.commits });
}
installedSdk() {
const roots = [this.appDataPath, path.join(this.appDataPath, 'frontend'), path.join(this.appDataPath, 'player')];
const found = [];

24
main.js
View File

@@ -84,6 +84,7 @@ function getReplacementsConfig() {
const updateClient = new UpdateClient(APPDATA_DIR);
const fileUpdater = new FileUpdater(APPDATA_DIR, updateClient);
fileUpdater.onNotice = (message) => sendToTauri('notice', { message });
let lastFilePercent = -1;
fileUpdater.onProgress = (downloaded, total) => {
const percent = total > 0 ? Math.round((downloaded / total) * 100) : 0;
@@ -222,7 +223,7 @@ async function start() {
debugLog(`Update check finished. Found: ${release ? release.version : 'None'}`);
} catch (e) {
debugLog(`Update check failed (offline?): ${e.message}`);
sendToTauri('status', { message: `Update server check skipped (${e.message})` });
sendToTauri('notice', { message: `Update check skipped: ${e.message}` });
}
const normalizeVer = (v) => (v || '').toString().replace(/^v/, '').trim();
@@ -242,7 +243,7 @@ async function start() {
const versionNewer = isNewer(release.version, localVersion);
const sameVerNewBuild = normalizeVer(release.version) === normalizeVer(localVersion) &&
!!release.sha256 && !!localSha && release.sha256 !== localSha;
const hasUpdate = versionNewer;
const hasUpdate = versionNewer || sameVerNewBuild;
if (isFirstRun) {
debugLog("First run with no local payload. Installing remote build...");
@@ -269,12 +270,14 @@ async function start() {
patcher.bridgeCredentials(serviceManager.credentialManager);
let ver = "1.0.0";
let manSha = null;
try {
const man = JSON.parse(fs.readFileSync(path.join(__dirname, 'manifest.json'), 'utf8'));
ver = man.version || ver;
manSha = man.sha256 || null;
} catch (e) { }
patcher.saveCurrentVersion(ver);
patcher.saveCurrentVersion(ver, manSha);
serviceManager.envConfig = getEnvConfig();
debugLog("Local payload extracted successfully.");
} else {
@@ -289,26 +292,29 @@ async function start() {
sendToTauri('status', { message: 'Checking UI files and SDK...' });
const [filesCheck, sdkCheck] = await Promise.all([
withTimeout(fileUpdater.checkFiles().catch((e) => {
debugLog(`File check failed: ${e.message}`);
withTimeout(fileUpdater.checkRepos().catch((e) => {
debugLog(`Live file check failed: ${e.message}`);
sendToTauri('notice', { message: `File check failed: ${e.message}` });
return null;
}), 180000),
}), 240000),
withTimeout(fileUpdater.checkSdk().catch((e) => {
debugLog(`SDK check failed: ${e.message}`);
sendToTauri('notice', { message: `SDK check failed: ${e.message}` });
return null;
}), 12000)
]);
debugLog(`Repo check result: ${filesCheck ? `${filesCheck.changes.length} change(s)` : 'failed or timed out'}`);
if (filesCheck && filesCheck.changes.length > 0) {
if (filesCheck && (filesCheck.changes.length > 0 || filesCheck.removed.length > 0)) {
sendToTauri('status', { message: `Updating ${filesCheck.changes.length} file(s)...` });
try {
await fileUpdater.applyFiles(filesCheck.changes, filesCheck.seq, getReplacementsConfig(), (file, done, total) => {
await fileUpdater.applyLive(filesCheck, getReplacementsConfig(), (file, done, total) => {
sendToTauri('extract-progress', { file, done, total, percent: Math.round((done / total) * 100) });
});
sendToTauri('status', { message: `Updated ${filesCheck.changes.length} file(s).` });
} catch (e) {
debugLog(`File update failed: ${e.message}`);
sendToTauri('status', { message: `File update failed: ${e.message}` });
sendToTauri('notice', { message: `File update failed: ${e.message}` });
}
}

3
package-lock.json generated
View File

@@ -11,6 +11,7 @@
"@tauri-apps/api": "2.12.1",
"@tauri-apps/plugin-opener": "2.7.0",
"@tauri-apps/plugin-process": "2.4.0",
"@tauri-apps/plugin-updater": "^2",
"@tauri-apps/plugin-shell": "2.5.0",
"@vyla-entertainment/sdk": "^1.20.0",
"adm-zip": "^0.6.1",
@@ -1535,4 +1536,4 @@
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "vyla-home",
"version": "1.1.50",
"version": "1.1.54",
"description": "Vyla Home Desktop App Launcher",
"main": "main.js",
"scripts": {

View File

@@ -1,45 +0,0 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const roots = { frontend: process.argv[2], player: process.argv[3] };
const outDir = path.join(__dirname, 'ui-updates');
const filesDir = path.join(outDir, 'files');
const keyPath = process.env.VYLA_UI_KEY || path.join(__dirname, 'ui-private.pem');
fs.rmSync(outDir, { recursive: true, force: true });
fs.mkdirSync(filesDir, { recursive: true });
fs.writeFileSync(path.join(outDir, '.gitattributes'), '* -text\n');
const files = [];
const walk = (src, rel) => {
for (const name of fs.readdirSync(src)) {
if (name === 'node_modules' || name === '.git') continue;
const full = path.join(src, name);
const r = `${rel}/${name}`;
const st = fs.lstatSync(full);
if (st.isDirectory()) {
walk(full, r);
} else if (st.isFile()) {
const data = fs.readFileSync(full);
const dest = path.join(filesDir, ...r.split('/'));
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.writeFileSync(dest, data);
files.push({ path: r, sha256: crypto.createHash('sha256').update(data).digest('hex'), size: data.length });
}
}
};
for (const [root, src] of Object.entries(roots)) {
if (!src || !fs.existsSync(src)) {
console.error(`Missing source dir for ${root}`);
process.exit(1);
}
walk(path.resolve(src), root);
}
const raw = Buffer.from(JSON.stringify({ seq: Math.floor(Date.now() / 1000), files }));
const sig = crypto.sign(null, raw, crypto.createPrivateKey(fs.readFileSync(keyPath)));
fs.writeFileSync(path.join(outDir, 'files-manifest.json'), raw);
fs.writeFileSync(path.join(outDir, 'files-manifest.sig'), sig.toString('base64'));
console.log(`${files.length} files signed`);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

2
src-tauri/Cargo.lock generated
View File

@@ -4073,7 +4073,7 @@ dependencies = [
[[package]]
name = "vyla-home"
version = "1.1.50"
version = "1.1.54"
dependencies = [
"objc2 0.5.2",
"serde",

View File

@@ -1,6 +1,6 @@
[package]
name = "vyla-home"
version = "1.1.50"
version = "1.1.54"
description = "Vyla Home Desktop App Launcher"
edition = "2021"

View File

@@ -2,6 +2,18 @@ fn main() {
println!("cargo:rerun-if-changed=src/main.rs");
println!("cargo:rerun-if-changed=tauri.conf.json");
println!("cargo:rerun-if-changed=capabilities");
for f in [
"main.js",
"service-manager.js",
"file-updater.js",
"patcher.js",
"crypto-utils.js",
"credential-manager.js",
"update-client.js",
] {
println!("cargo:rerun-if-changed=../{}", f);
}
println!("cargo:rerun-if-changed=../config");
if std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default() == "windows" {
if let (Ok(manifest_dir), Ok(out_dir)) = (std::env::var("CARGO_MANIFEST_DIR"), std::env::var("OUT_DIR")) {

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Vyla Home",
"version": "1.1.50",
"version": "1.1.54",
"identifier": "com.vyla.home",
"build": {
"frontendDist": "../ui"

View File

@@ -85,7 +85,6 @@
height: 70px;
object-fit: contain;
margin-bottom: 22px;
filter: drop-shadow(0 4px 12px rgba(0, 0, 0, 0.35));
user-select: none;
-webkit-user-drag: none;
}
@@ -101,7 +100,6 @@
flex-wrap: wrap;
justify-content: center;
align-items: center;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
}
.status-char {
@@ -162,20 +160,16 @@
height: 100%;
width: 0%;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 0 12px rgba(255, 255, 255, 0.5);
border-radius: 9999px;
transition: width 0.35s cubic-bezier(0.2, 0.8, 0.2, 1);
}
#debug-log-box {
display: none;
width: 100%;
max-height: 150px;
overflow-y: auto;
background: rgba(255, 59, 48, 0.18);
border: 1px solid rgba(255, 59, 48, 0.35);
border-radius: 10px;
padding: 12px;
font-family: ui-monospace, SFMono-Regular, monospace;
@@ -187,6 +181,20 @@
margin-bottom: 16px;
user-select: text;
}
.notice-line {
display: none;
max-width: 100%;
margin-bottom: 14px;
padding: 8px 12px;
border-radius: 10px;
background: rgba(255, 204, 102, 0.14);
color: #ffd98a;
font-size: 12px;
font-weight: 500;
text-align: center;
word-break: break-word;
}
</style>
</head>
@@ -197,9 +205,9 @@
<div class="loader-container">
<div class="loader-card">
<img class="app-icon" src="https://gitlab.com/vyla-entertainment/home/-/raw/main/build/white_favicon.png"
alt="Vyla" draggable="false">
<img class="app-icon" src="/white_favicon.png" alt="Vyla" draggable="false">
<div class="status-text" id="status-message">Starting runtime engine...</div>
<div class="notice-line" id="notice-line"></div>
<div id="debug-log-box"></div>
<div class="file-line" id="file-line"></div>
<div class="progress-track" id="progress-track" style="display: none;">
@@ -900,6 +908,19 @@ void main() {
if (data && data.message) setStatus(data.message);
});
eventApi.listen("notice", (e) => {
const data = e.payload || e;
const el = document.getElementById("notice-line");
if (!el) return;
if (data && data.message) {
el.textContent = data.message;
el.style.display = "block";
} else {
el.textContent = "";
el.style.display = "none";
}
});
eventApi.listen("download-progress", (e) => {
const data = e.payload || e;
if (data && typeof data.percent === "number") {

View File

Before

Width:  |  Height:  |  Size: 223 KiB

After

Width:  |  Height:  |  Size: 223 KiB