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'); const debugLogPath = path.join(os.homedir(), 'Downloads', 'vyla-node-debug.log'); function debugLog(msg) { try { fs.appendFileSync(debugLogPath, `[NODE] ${msg}\n`, 'utf8'); } catch (e) { } } debugLog("--- Node.js Process Started ---"); 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 }); debugLog(`AppData Dir: ${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); function sendToTauri(event, data) { debugLog(`Emitting to Tauri: [${event}] -> ${JSON.stringify(data)}`); process.stdout.write(JSON.stringify({ event, data }) + '\n'); } process.on('unhandledRejection', (reason) => { const message = reason && reason.message ? reason.message : String(reason); debugLog(`UNHANDLED REJECTION: ${message}`); sendToTauri('error', { message }); }); process.on('uncaughtException', (err) => { debugLog(`UNCAUGHT EXCEPTION: ${err.message}`); sendToTauri('error', { message: err.message }); }); async function start() { debugLog("Inside start() function"); sendToTauri('status', { message: 'Initializing patcher...' }); 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 { debugLog(`Attempting to download update (Attempt ${attempts})...`); sendToTauri('status', { message: `Downloading update (Attempt ${attempts}/3)...` }); await updateClient.downloadUpdate(release, tempZipPath, (downloaded, total) => { const percent = total > 0 ? Math.round((downloaded / total) * 100) : 0; sendToTauri('download-progress', { downloaded, total, percent }); }); debugLog('Download complete. Verifying checksum...'); sendToTauri('status', { message: 'Verifying payload checksum...' }); if (!patcher.verifyChecksum(tempZipPath, release.sha256)) { throw new Error('SHA-256 checksum verification failed.'); } debugLog('Checksum verified. Extracting...'); sendToTauri('status', { message: 'Extracting update files...' }); patcher.purgeOldCode(); patcher.extractZipPayload(tempZipPath); if (!patcher.verifyExtraction()) { throw new Error('Extraction verification failed: critical startup files missing.'); } debugLog('Extraction complete. Applying patches...'); sendToTauri('status', { message: 'Applying patches & configuring...' }); patcher.applyPatches(replacementsConfig); patcher.bridgeCredentials(serviceManager.credentialManager); patcher.saveCurrentVersion(release.version, release.sha256); if (fs.existsSync(tempZipPath)) { fs.unlinkSync(tempZipPath); } downloadSuccess = true; debugLog('Update installed successfully!'); } catch (err) { debugLog(`Download/Install error on attempt ${attempts}: ${err.message}`); if (fs.existsSync(tempZipPath)) { try { fs.unlinkSync(tempZipPath); } catch (e) { } } 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(); debugLog(`Is First Run: ${isFirstRun}, Local Version: ${localVersion}`); sendToTauri('status', { message: 'Checking for updates...' }); let release = null; try { debugLog("Fetching updates from updateClient..."); release = await updateClient.checkForUpdates(); debugLog(`Update check finished. Found: ${release ? release.version : 'None'}`); } catch (e) { debugLog(`Update check failed (offline?): ${e.message}`); } if (release) { const hasUpdate = release.version !== localVersion || (release.sha256 && release.sha256 !== localSha); if (isFirstRun || hasUpdate) { debugLog("Update required or first run. Installing remote build..."); await installBuild(release); } else { debugLog("Already on the latest version."); } } else if (isFirstRun) { debugLog("No remote release found, but it's the first run. Looking for local update-payload.zip..."); const localZip = path.join(__dirname, 'update-payload.zip'); if (fs.existsSync(localZip)) { sendToTauri('status', { message: 'Extracting initial local payload...' }); patcher.purgeOldCode(); patcher.extractZipPayload(localZip); patcher.applyPatches(replacementsConfig); patcher.bridgeCredentials(serviceManager.credentialManager); patcher.saveCurrentVersion("1.0.0"); debugLog("Local payload extracted successfully."); } else { debugLog("No local payload found!"); } } if (!patcher.verifyExtraction()) { throw new Error('No application files found. Check your internet connection and try again.'); } sendToTauri('status', { message: 'Loading credentials...' }); serviceManager.credentialManager.loadCredentials(); sendToTauri('status', { message: 'Starting backend services...' }); await serviceManager.startServices([ { name: 'auth-proxy', relativeDir: 'auth-proxy' }, { name: 'stream-api', relativeDir: 'stream-api' }, ]); debugLog("Backend services spawn requested."); 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, '0.0.0.0'); debugLog("Player Express running on 3001"); sendToTauri('status', { message: 'Waiting for ports 3000, 3001, 7860...' }); await serviceManager.waitForPorts([3000, 3001, 7860]); debugLog("Ports 3000, 3001, 7860 are ready."); 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, '0.0.0.0', () => { debugLog("Frontend Express running on 4890. Sending READY to Tauri."); sendToTauri('ready', { url: `http://localhost:${FRONTEND_PORT}` }); }); } catch (error) { debugLog(`Fatal Start Error: ${error.message}`); sendToTauri('error', { message: error.message }); } } process.on('exit', () => { if (serviceManager) { for (const name of Object.keys(serviceManager.processes)) { const proc = serviceManager.processes[name]; if (proc && proc.child) { try { proc.child.kill('SIGKILL'); } catch (e) { } } } } }); process.on('SIGINT', async () => { if (expressServer) expressServer.close(); if (playerExpressServer) playerExpressServer.close(); if (serviceManager) await serviceManager.stopAll(); process.exit(0); }); start();