const { app, BrowserWindow, ipcMain, Menu } = require('electron'); const path = require('path'); const fs = require('fs'); const express = require('express'); const ServiceManager = require('./service-manager'); const Patcher = require('./patcher'); const UpdateClient = require('./update-client'); let mainWindow; let serviceManager; let expressApp; let expressServer; let playerExpressApp; let playerExpressServer; const gotLock = app.requestSingleInstanceLock(); if (!gotLock) { app.quit(); } else { app.on('second-instance', () => { if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.focus(); } }); } const APPDATA_DIR = path.join(app.getPath('userData'), 'vyla-home-files'); fs.mkdirSync(APPDATA_DIR, { recursive: true }); const envConfig = JSON.parse(fs.readFileSync(path.join(__dirname, 'config/env.json'), 'utf8')); const replacementsConfig = JSON.parse(fs.readFileSync(path.join(__dirname, 'config/replacements.json'), 'utf8')); const updateClient = new UpdateClient(APPDATA_DIR); process.on('unhandledRejection', (reason) => { const message = reason && reason.message ? reason.message : String(reason); fs.appendFileSync(path.join(APPDATA_DIR, 'crash.log'), `[${new Date().toISOString()}] UNHANDLED REJECTION: ${message}\n`, 'utf8'); }); process.on('uncaughtException', (err) => { fs.appendFileSync(path.join(APPDATA_DIR, 'crash.log'), `[${new Date().toISOString()}] UNCAUGHT EXCEPTION: ${err.message}\n`, 'utf8'); }); async function createWindow() { const asciiLogo = fs.readFileSync(path.join(__dirname, 'build', 'logo.txt'), 'utf8'); mainWindow = new BrowserWindow({ width: 1280, height: 800, title: 'Vyla Home', icon: path.join(__dirname, 'build', 'icon.ico'), titleBarStyle: 'hidden', titleBarOverlay: { color: '#00000000', symbolColor: '#ffffff', height: 40 }, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, 'preload.js') } }); Menu.setApplicationMenu(null); mainWindow.webContents.setWindowOpenHandler(({ url }) => { mainWindow.loadURL(url); return { action: 'deny' }; }); mainWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(` Vyla Home
`)); const patcher = new Patcher(APPDATA_DIR); serviceManager = new ServiceManager(APPDATA_DIR, envConfig); const showUpdatePrompt = (currentVersion, newVersion) => { return new Promise((resolve) => { ipcMain.removeAllListeners('update-choice'); ipcMain.once('update-choice', (event, choice) => { resolve(choice); }); mainWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(` Update Available

Update Available

You're running version ${currentVersion}. Version ${newVersion} is available.\nInstalling will fetch the latest build.

`)); }); }; const handleFatalError = (errMessage) => { mainWindow.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(` Startup Error

Couldn't start Vyla Home

${errMessage}

`)); }; const installBuild = async (release) => { const tempZipPath = path.join(APPDATA_DIR, 'download-temp.zip'); let attempts = 0; let downloadSuccess = false; while (attempts < 3 && !downloadSuccess) { attempts++; try { await updateClient.downloadUpdate(release, tempZipPath, (downloaded, total) => { const percent = Math.round((downloaded / total) * 100); }); if (!patcher.verifyChecksum(tempZipPath, release.sha256)) { throw new Error('SHA-256 checksum verification failed.'); } patcher.purgeOldCode(); patcher.extractZipPayload(tempZipPath); if (!patcher.verifyExtraction()) { throw new Error('Extraction verification failed: critical startup files missing.'); } patcher.applyPatches(replacementsConfig); patcher.bridgeCredentials(serviceManager.credentialManager); patcher.saveCurrentVersion(release.version); fs.unlinkSync(tempZipPath); downloadSuccess = true; } catch (err) { if (fs.existsSync(tempZipPath)) fs.unlinkSync(tempZipPath); if (attempts >= 3) { throw new Error(`Failed to download and verify build after 3 attempts: ${err.message}`); } await new Promise(r => setTimeout(r, 2000)); } } }; try { const isFirstRun = !patcher.verifyExtraction(); const localVersion = patcher.getCurrentVersion(); const release = await updateClient.checkForUpdates(); if (isFirstRun) { await installBuild(release); } else if (release.version !== localVersion) { const choice = await showUpdatePrompt(localVersion, release.version); if (choice === 'install') { await installBuild(release); } } if (!patcher.verifyExtraction()) { throw new Error('No application files found. Check your internet connection and try again.'); } serviceManager.credentialManager.loadCredentials(); await serviceManager.startServices([ { name: 'auth-proxy', relativeDir: 'auth-proxy' }, { name: 'stream-api', relativeDir: 'stream-api' }, ]); const playerDir = path.join(APPDATA_DIR, 'player'); playerExpressApp = express(); playerExpressApp.use(express.static(playerDir, { maxAge: '1d', etag: true })); playerExpressApp.get('*', (req, res) => { res.sendFile(path.join(playerDir, 'index.html')); }); playerExpressServer = playerExpressApp.listen(3001); await serviceManager.waitForPorts([3000, 3001, 7860]); const healthChecks = [ { port: 3001, name: 'player' }, { port: 3000, name: 'auth-proxy' }, { port: 7860, name: 'stream-api' }, ]; for (const { port, name } of healthChecks) { try { await serviceManager.checkHealth(port, '/'); } catch (e) { serviceManager.logToFile(name, `Health check on / did not pass, continuing anyway: ${e.message}`); } } const frontendDir = path.join(APPDATA_DIR, 'frontend'); expressApp = express(); expressApp.use(express.static(frontendDir, { maxAge: '1d', etag: true })); expressApp.get('*', (req, res) => { res.sendFile(path.join(frontendDir, 'index.html')); }); const FRONTEND_PORT = 4890; expressServer = expressApp.listen(FRONTEND_PORT, () => { mainWindow.loadURL(`http://localhost:${FRONTEND_PORT}`); }); } catch (error) { if (serviceManager) { await serviceManager.stopAll().catch(() => { }); } handleFatalError(error.message); } } if (gotLock) { app.whenReady().then(createWindow); } app.on('window-all-closed', async () => { if (expressServer) expressServer.close(); if (playerExpressServer) playerExpressServer.close(); if (serviceManager) { await serviceManager.stopAll(); } if (process.platform !== 'darwin') { app.quit(); } });