mirror of
https://gitlab.com/vyla-entertainment/home.git
synced 2026-11-09 19:57:20 -05:00
406 lines
16 KiB
JavaScript
406 lines
16 KiB
JavaScript
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 defaultAppData = 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
|
|
: defaultAppData;
|
|
|
|
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(
|
|
'</head>',
|
|
`<script>
|
|
document.addEventListener('click', function(e) {
|
|
const a = e.target.closest('a[target]');
|
|
if (a && a.target === '_blank') a.removeAttribute('target');
|
|
}, true);
|
|
window.open = function(url) {
|
|
if (url) window.location.href = url;
|
|
return window;
|
|
};
|
|
</script></head>`
|
|
);
|
|
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...' });
|
|
const isFirstRun = !patcher.verifyExtraction();
|
|
const localVersion = patcher.getCurrentVersion();
|
|
const localSha = patcher.getCurrentSha();
|
|
debugLog(`Is First Run: ${isFirstRun}, Local Version: ${localVersion}`);
|
|
|
|
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);
|
|
patcher.saveCurrentVersion("1.0.0");
|
|
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(); |