const path = require('path'); const fs = require('fs'); const os = require('os'); const express = require('express'); const ServiceManager = require('./service-manager'); const Patcher = require('./patcher'); const UpdateClient = require('./update-client'); let serviceManager; let expressApp; let expressServer; let playerExpressApp; let playerExpressServer; const APPDATA_DIR = process.env.APPDATA_PATH || path.join(os.homedir(), 'AppData', 'Roaming', 'vyla-home', 'vyla-home-files'); fs.mkdirSync(APPDATA_DIR, { recursive: true }); console.log(`[DEBUG] AppData directory: ${APPDATA_DIR}`); 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); console.error(`[ERROR] Unhandled Rejection: ${message}`); fs.appendFileSync(path.join(APPDATA_DIR, 'crash.log'), `[${new Date().toISOString()}] UNHANDLED REJECTION: ${message}\n`, 'utf8'); }); process.on('uncaughtException', (err) => { console.error(`[ERROR] Uncaught Exception: ${err.message}`); fs.appendFileSync(path.join(APPDATA_DIR, 'crash.log'), `[${new Date().toISOString()}] UNCAUGHT EXCEPTION: ${err.message}\n`, 'utf8'); }); function sendToTauri(event, data) { process.stdout.write(JSON.stringify({ event, data }) + '\n'); } async function start() { console.log('[DEBUG] Initializing patcher and service manager...'); const patcher = new Patcher(APPDATA_DIR); serviceManager = new ServiceManager(APPDATA_DIR, envConfig); 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 { console.log(`[DEBUG] Downloading update, attempt ${attempts}...`); await updateClient.downloadUpdate(release, tempZipPath, (downloaded, total) => { const percent = Math.round((downloaded / total) * 100); sendToTauri('download-progress', { downloaded, total, percent }); }); if (!patcher.verifyChecksum(tempZipPath, release.sha256)) { throw new Error('SHA-256 checksum verification failed.'); } console.log('[DEBUG] Extracting payload and applying patches...'); 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, release.sha256); 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 localSha = patcher.getCurrentSha(); console.log(`[DEBUG] Local version: ${localVersion}, isFirstRun: ${isFirstRun}`); let release = null; try { release = await updateClient.checkForUpdates(); } catch (e) { console.warn('[DEBUG] Could not check for remote updates:', e.message); } if (release) { const hasUpdate = release.version !== localVersion || (release.sha256 && release.sha256 !== localSha); if (isFirstRun || hasUpdate) { console.log('[DEBUG] Performing update install from release...'); await installBuild(release); } } else if (isFirstRun) { const localZip = path.join(__dirname, 'update-payload.zip'); console.log(`[DEBUG] Checking local zip at: ${localZip} (exists: ${fs.existsSync(localZip)})`); if (fs.existsSync(path.join(__dirname, 'update-payload.zip'))) { const localZip = path.join(__dirname, 'update-payload.zip'); console.log('[DEBUG] Syncing local update-payload.zip to AppData...'); patcher.purgeOldCode(); patcher.extractZipPayload(localZip); patcher.applyPatches(replacementsConfig); patcher.bridgeCredentials(serviceManager.credentialManager); patcher.saveCurrentVersion("1.0.0"); } } const verified = patcher.verifyExtraction(); console.log(`[DEBUG] Payload verification status: ${verified}`); if (!verified) { throw new Error('No application files found. Build the payload first using "npm run build:payload" or check your internet connection.'); } console.log('[DEBUG] Loading credentials...'); serviceManager.credentialManager.loadCredentials(); console.log('[DEBUG] Starting auth-proxy and stream-api...'); await serviceManager.startServices([ { name: 'auth-proxy', relativeDir: 'auth-proxy' }, { name: 'stream-api', relativeDir: 'stream-api' }, ]); const playerDir = path.join(APPDATA_DIR, 'player'); console.log(`[DEBUG] Starting Player Express server on port 3001 from: ${playerDir}`); 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, '0.0.0.0', () => { console.log('[DEBUG] Player Express server listening on 3001'); }); console.log('[DEBUG] Waiting for ports [3000, 3001, 7860]...'); await serviceManager.waitForPorts([3000, 3001, 7860]); console.log('[DEBUG] All ports ready!'); const frontendDir = path.join(APPDATA_DIR, 'frontend'); console.log(`[DEBUG] Starting Frontend Express server on port 4890 from: ${frontendDir}`); 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, '0.0.0.0', () => { console.log(`[DEBUG] Frontend Express server active at http://localhost:${FRONTEND_PORT}`); sendToTauri('ready', { url: `http://localhost:${FRONTEND_PORT}` }); }); } catch (error) { if (serviceManager) { await serviceManager.stopAll().catch(() => { }); } console.error('[DEBUG] Fatal error caught:', error.message); sendToTauri('error', { message: error.message }); } } process.on('SIGINT', async () => { if (expressServer) expressServer.close(); if (playerExpressServer) playerExpressServer.close(); if (serviceManager) await serviceManager.stopAll(); process.exit(0); }); start();