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'); function getDefaultAppData() { if (process.platform === 'darwin') { return path.join(os.homedir(), 'Library', 'Application Support', 'vyla-home', 'vyla-home-files'); } if (process.platform === 'linux') { return path.join(os.homedir(), '.local', 'share', 'vyla-home', 'vyla-home-files'); } return path.join(os.homedir(), 'AppData', 'Roaming', 'vyla-home', 'vyla-home-files'); } const APPDATA_DIR = (process.env.APPDATA_PATH && !process.env.APPDATA_PATH.includes('com.vyla.home')) ? process.env.APPDATA_PATH : getDefaultAppData(); const LOGS_DIR = path.join(APPDATA_DIR, 'logs'); try { fs.mkdirSync(LOGS_DIR, { recursive: true }); } catch (e) { } const debugLogPath = path.join(LOGS_DIR, 'node-debug.log'); function debugLog(msg) { try { fs.appendFile(debugLogPath, `[NODE] ${msg}\n`, 'utf8', () => { }); } catch (e) { } } debugLog("--- Node.js Process Started ---"); let serviceManager; let expressApp; let expressServer; let playerExpressApp; let playerExpressServer; debugLog(`AppData Dir: ${APPDATA_DIR}`); function sendToTauri(event, data) { debugLog(`Emitting to Tauri: [${event}] -> ${JSON.stringify(data)}`); process.stdout.write(JSON.stringify({ event, data }) + '\n'); } function sameTabNavMiddleware(rootDir) { return (req, res, next) => { if (req.method !== 'GET') return next(); const isHtmlRequest = req.path === '/' || req.path.endsWith('/') || req.path.endsWith('.html'); if (!isHtmlRequest) return next(); const filePath = req.path === '/' || req.path.endsWith('/') ? path.join(rootDir, req.path, 'index.html') : path.join(rootDir, req.path); fs.readFile(filePath, 'utf8', (err, data) => { if (err) return next(); const injected = data.replace( '', `` ); res.set('Content-Type', 'text/html'); res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private'); res.set('Expires', '-1'); res.set('Pragma', 'no-cache'); res.send(injected); }); }; } const cmdApp = express(); cmdApp.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'); res.header('Access-Control-Allow-Headers', '*'); if (req.method === 'OPTIONS') return res.sendStatus(200); next(); }); cmdApp.get('/__tauri_cmd/:cmd', (req, res) => { debugLog(`[CMD-SERVER:4892] Command received: ${req.params.cmd} (window: ${req.query.window || 'main'})`); sendToTauri('tauri-cmd', { cmd: req.params.cmd, url: req.query.url, window: req.query.window }); res.sendStatus(200); }); cmdApp.listen(4892, '127.0.0.1', () => debugLog("[CMD-SERVER] Dedicated command server listening on 127.0.0.1:4892")); process.stdin.resume(); process.stdin.on('end', () => { debugLog('stdin closed - parent died. Self-terminating...'); process.exit(0); }); process.stdin.on('error', () => { process.exit(0); }); process.on('uncaughtException', (err) => { debugLog(`CRITICAL UNCAUGHT EXCEPTION: ${err.stack || err.message}`); if (err.code === 'EPIPE' || err.code === 'EOF') { debugLog('Pipe to parent closed - parent process gone, exiting'); process.exit(0); } try { sendToTauri('error', { message: err.message }); } catch (e) { } }); process.on('unhandledRejection', (reason) => { const msg = reason && reason.stack ? reason.stack : String(reason); debugLog(`CRITICAL UNHANDLED REJECTION: ${msg}`); sendToTauri('error', { message: reason && reason.message ? reason.message : String(reason) }); }); function getEnvConfig() { try { const appPath = path.join(APPDATA_DIR, 'config/env.json'); if (fs.existsSync(appPath)) return JSON.parse(fs.readFileSync(appPath, 'utf8')); return JSON.parse(fs.readFileSync(path.join(__dirname, 'config/env.json'), 'utf8')); } catch (e) { return {}; } } function getReplacementsConfig() { try { const appPath = path.join(APPDATA_DIR, 'config/replacements.json'); if (fs.existsSync(appPath)) return JSON.parse(fs.readFileSync(appPath, 'utf8')); return JSON.parse(fs.readFileSync(path.join(__dirname, 'config/replacements.json'), 'utf8')); } catch (e) { return {}; } } const updateClient = new UpdateClient(APPDATA_DIR); async function start() { debugLog("Inside start() function"); sendToTauri('status', { message: 'Loading core configuration files...' }); const patcher = new Patcher(APPDATA_DIR); serviceManager = new ServiceManager(APPDATA_DIR, getEnvConfig()); const installBuild = async (release) => { const tempZipPath = path.join(APPDATA_DIR, 'download-temp.zip'); if (fs.existsSync(tempZipPath)) { try { fs.unlinkSync(tempZipPath); } catch (e) { } } let attempts = 0; let downloadSuccess = false; while (attempts < 3 && !downloadSuccess) { attempts++; try { debugLog(`Attempting to download update (Attempt ${attempts})...`); sendToTauri('status', { message: `Connecting to download servers (Attempt ${attempts}/3)...` }); let lastPercent = -1; await updateClient.downloadUpdate(release, tempZipPath, (downloaded, total) => { const percent = total > 0 ? Math.round((downloaded / total) * 100) : 0; if (percent !== lastPercent) { lastPercent = percent; sendToTauri('download-progress', { downloaded, total, percent }); } }); debugLog('Download complete. Verifying checksum...'); sendToTauri('status', { message: 'Verifying SHA-256 integrity...' }); if (!patcher.verifyChecksum(tempZipPath, release.sha256)) { throw new Error('SHA-256 checksum verification failed.'); } debugLog('Checksum verified. Extracting...'); sendToTauri('status', { message: 'Unpacking update archive...' }); patcher.purgeOldCode(); patcher.extractZipPayload(tempZipPath); sendToTauri('status', { message: 'Verifying file integrity...' }); if (!patcher.verifyExtraction()) { throw new Error('Extraction verification failed: critical startup files missing.'); } debugLog('Extraction complete. Applying patches...'); sendToTauri('status', { message: 'Applying configuration patches...' }); patcher.applyPatches(getReplacementsConfig()); sendToTauri('status', { message: 'Migrating security credentials...' }); patcher.bridgeCredentials(serviceManager.credentialManager); patcher.saveCurrentVersion(release.version, release.sha256); serviceManager.envConfig = getEnvConfig(); if (fs.existsSync(tempZipPath)) { fs.unlinkSync(tempZipPath); } downloadSuccess = true; debugLog('Update installed successfully!'); } catch (err) { debugLog(`Download/Install error on attempt ${attempts}: ${err.message}`); sendToTauri('status', { message: `Install error: ${err.message}. Retrying...` }); 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 { sendToTauri('status', { message: 'Inspecting local workspace and installed files...' }); let isFirstRun = !patcher.verifyExtraction(); let localVersion = patcher.getCurrentVersion(); let localSha = patcher.getCurrentSha(); debugLog(`Is First Run: ${isFirstRun}, Local Version: ${localVersion}, Local SHA: ${localSha}`); const localZipPath = path.join(__dirname, 'update-payload.zip'); const localManifestPath = path.join(__dirname, 'manifest.json'); let localZipManifest = null; if (fs.existsSync(localZipPath) && fs.existsSync(localManifestPath)) { try { localZipManifest = JSON.parse(fs.readFileSync(localManifestPath, 'utf8')); } catch (e) { } } if (localZipManifest && (isFirstRun || localZipManifest.sha256 !== localSha)) { debugLog("New local payload detected! Applying local build..."); sendToTauri('status', { message: 'Extracting local development payload...' }); patcher.purgeOldCode(); patcher.extractZipPayload(localZipPath); patcher.applyPatches(getReplacementsConfig()); patcher.bridgeCredentials(serviceManager.credentialManager); patcher.saveCurrentVersion(localZipManifest.version, localZipManifest.sha256); serviceManager.envConfig = getEnvConfig(); isFirstRun = false; localVersion = localZipManifest.version; localSha = localZipManifest.sha256; debugLog("Local payload applied successfully."); } sendToTauri('status', { message: 'Connecting to update servers...' }); 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}`); sendToTauri('status', { message: `Update server check skipped (${e.message})` }); } if (release) { const hasUpdate = release.version !== localVersion || (release.sha256 && release.sha256 !== localSha); if (isFirstRun) { debugLog("First run. Installing remote build..."); sendToTauri('status', { message: 'First run detected. Preparing initial installation...' }); await installBuild(release); } else if (hasUpdate) { debugLog("Update available. Prompting user..."); sendToTauri('status', { message: `New update available (${release.version}). Awaiting confirmation...` }); const tempApp = express(); let userDecision = null; tempApp.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'); res.header('Access-Control-Allow-Headers', '*'); if (req.method === 'OPTIONS') return res.sendStatus(200); next(); }); tempApp.get('/update/info', (req, res) => { res.json({ available: true, current: localVersion, latest: release.version }); }); tempApp.post('/update/yes', (req, res) => { userDecision = true; res.sendStatus(200); }); tempApp.post('/update/no', (req, res) => { userDecision = false; res.sendStatus(200); }); const tempServer = await new Promise((resolve) => { const s = tempApp.listen(4891, '127.0.0.1', () => resolve(s)); }); sendToTauri('update-available', { current: localVersion, latest: release.version }); while (userDecision === null) { await new Promise(r => setTimeout(r, 25)); } await new Promise((resolve) => tempServer.close(resolve)); if (userDecision) { debugLog("User accepted update."); await installBuild(release); } else { debugLog("User skipped update."); sendToTauri('status', { message: 'Update skipped. Continuing startup...' }); } } else { debugLog("Already on the latest version."); sendToTauri('status', { message: 'Up to date.' }); } } else if (isFirstRun) { debugLog("No remote release found, but it's the first run. Looking for local update-payload.zip..."); sendToTauri('status', { message: 'Looking for local payload archive...' }); const localZip = path.join(__dirname, 'update-payload.zip'); if (fs.existsSync(localZip)) { sendToTauri('status', { message: 'Extracting local baseline payload...' }); patcher.purgeOldCode(); patcher.extractZipPayload(localZip); patcher.applyPatches(getReplacementsConfig()); patcher.bridgeCredentials(serviceManager.credentialManager); let ver = "1.0.0"; try { const man = JSON.parse(fs.readFileSync(path.join(__dirname, 'manifest.json'), 'utf8')); ver = man.version || ver; } catch (e) { } patcher.saveCurrentVersion(ver); serviceManager.envConfig = getEnvConfig(); debugLog("Local payload extracted successfully."); } else { debugLog("No local payload found!"); } } sendToTauri('status', { message: 'Validating critical components...' }); if (!patcher.verifyExtraction()) { throw new Error('No application files found. Check your internet connection and try again.'); } sendToTauri('status', { message: 'Loading encrypted credentials...' }); serviceManager.credentialManager.loadCredentials(); sendToTauri('status', { message: 'Starting background services (auth-proxy, stream-api)...' }); await serviceManager.startServices([ { name: 'auth-proxy', relativeDir: 'auth-proxy' }, { name: 'stream-api', relativeDir: 'stream-api' }, ]); debugLog("Backend services spawn requested."); sendToTauri('status', { message: 'Binding player web server on port 3001...' }); const playerDir = path.join(APPDATA_DIR, 'player'); playerExpressApp = express(); playerExpressApp.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'); res.header('Access-Control-Allow-Headers', '*'); if (req.method === 'OPTIONS') return res.sendStatus(200); next(); }); playerExpressApp.get('/__tauri_cmd/:cmd', (req, res) => { debugLog(`[PLAYER-SERVER:3001] Command received: ${req.params.cmd} (window: ${req.query.window || 'main'})`); sendToTauri('tauri-cmd', { cmd: req.params.cmd, url: req.query.url, window: req.query.window }); res.sendStatus(200); }); playerExpressApp.use(sameTabNavMiddleware(playerDir)); playerExpressApp.use(express.static(playerDir, { etag: false, setHeaders: (res) => { res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private'); } })); playerExpressApp.get('*', (req, res) => { res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private'); res.sendFile(path.join(playerDir, 'index.html')); }); playerExpressServer = playerExpressApp.listen(3001, '127.0.0.1'); debugLog("Player Express running on 3001"); sendToTauri('status', { message: 'Waiting for microservices to bind ports (3000, 3001, 7860)...' }); await serviceManager.waitForPorts([3000, 3001, 7860]); debugLog("Ports 3000, 3001, 7860 are ready."); sendToTauri('status', { message: 'Binding UI server on port 4890...' }); const frontendDir = path.join(APPDATA_DIR, 'frontend'); expressApp = express(); expressApp.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'); res.header('Access-Control-Allow-Headers', '*'); if (req.method === 'OPTIONS') return res.sendStatus(200); next(); }); expressApp.get('/__tauri_cmd/:cmd', (req, res) => { debugLog(`[FRONTEND-SERVER:4890] Command received: ${req.params.cmd} (window: ${req.query.window || 'main'})`); sendToTauri('tauri-cmd', { cmd: req.params.cmd, url: req.query.url, window: req.query.window }); res.sendStatus(200); }); expressApp.use(sameTabNavMiddleware(frontendDir)); expressApp.use(express.static(frontendDir, { etag: false, setHeaders: (res) => { res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private'); } })); expressApp.get('*', (req, res) => { res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private'); res.sendFile(path.join(frontendDir, 'index.html')); }); const FRONTEND_PORT = 4890; expressApp.use('/build', express.static(path.join(__dirname, 'build'))); if (playerExpressApp) { playerExpressApp.use('/build', express.static(path.join(__dirname, 'build'))); } expressServer = expressApp.listen(FRONTEND_PORT, '127.0.0.1', () => { sendToTauri('status', { message: 'All backend services running and healthy.' }); 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 && proc.child.pid) { try { if (process.platform === 'win32') { require('child_process').spawnSync('taskkill', ['/F', '/T', '/PID', proc.child.pid.toString()], { stdio: 'ignore', windowsHide: true }); } else { process.kill(-proc.child.pid, 'SIGKILL'); } } catch (e) { } } } } }); process.on('SIGINT', () => { process.exit(0); }); process.on('SIGTERM', () => { process.exit(0); }); start();