mirror of
https://gitlab.com/vyla-entertainment/go.git
synced 2026-11-09 13:07:21 -05:00
200 lines
6.0 KiB
JavaScript
200 lines
6.0 KiB
JavaScript
const { spawn } = require('child_process');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const CredentialManager = require('./credential-manager');
|
|
|
|
class ServiceManager {
|
|
constructor(appDataPath, envConfig) {
|
|
this.appDataPath = appDataPath;
|
|
this.envConfig = envConfig;
|
|
this.credentialManager = new CredentialManager(appDataPath);
|
|
this.processes = {};
|
|
this.restartCounts = {};
|
|
this.stableTimers = {};
|
|
this.isStopping = false;
|
|
|
|
this.logsDir = path.join(appDataPath, 'logs');
|
|
fs.mkdirSync(this.logsDir, { recursive: true });
|
|
}
|
|
|
|
logToFile(name, line) {
|
|
try {
|
|
const logPath = path.join(this.logsDir, `${name}.log`);
|
|
const timestamp = new Date().toISOString();
|
|
fs.appendFile(logPath, `[${timestamp}] ${line}\n`, 'utf8', () => { });
|
|
process.stdout.write(JSON.stringify({ event: "service-log", data: { service: name, message: line } }) + '\n');
|
|
} catch (e) {
|
|
}
|
|
}
|
|
|
|
async startService(name, relativeDir, startScript = 'server.js') {
|
|
if (this.processes[name]) {
|
|
return;
|
|
}
|
|
|
|
const serviceDir = path.join(this.appDataPath, relativeDir);
|
|
const scriptPath = path.join(serviceDir, startScript);
|
|
|
|
if (!fs.existsSync(scriptPath)) {
|
|
throw new Error(`Service script not found: ${scriptPath}`);
|
|
}
|
|
|
|
let tokenSecret = process.env.TOKEN_SECRET ||
|
|
this.credentialManager.getCredential('player', 'TOKEN_SECRET');
|
|
|
|
if (!tokenSecret || tokenSecret.length < 32) {
|
|
tokenSecret = require('crypto').randomBytes(32).toString('hex');
|
|
this.credentialManager.setCredential('player', 'TOKEN_SECRET', tokenSecret);
|
|
}
|
|
|
|
const env = {
|
|
...process.env,
|
|
TOKEN_SECRET: tokenSecret,
|
|
...(this.envConfig[name] || {}),
|
|
...(this.credentialManager.getServiceCredentials(name) || {})
|
|
};
|
|
|
|
this.logToFile(name, `--- Starting ${name} in ${serviceDir} ---`);
|
|
|
|
const nodeExecutable = process.env.NODE_EXE || 'node';
|
|
|
|
const child = spawn(nodeExecutable, [scriptPath], {
|
|
cwd: serviceDir,
|
|
env,
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
shell: false
|
|
});
|
|
|
|
this.processes[name] = {
|
|
child,
|
|
relativeDir,
|
|
startScript,
|
|
status: 'starting'
|
|
};
|
|
|
|
if (this.stableTimers[name]) {
|
|
clearTimeout(this.stableTimers[name]);
|
|
}
|
|
this.stableTimers[name] = setTimeout(() => {
|
|
this.restartCounts[name] = 0;
|
|
}, 60000);
|
|
|
|
child.stdout.on('data', (data) => {
|
|
const text = data.toString().trim();
|
|
this.logToFile(name, `STDOUT: ${text}`);
|
|
});
|
|
|
|
child.stderr.on('data', (data) => {
|
|
const text = data.toString().trim();
|
|
this.logToFile(name, `STDERR: ${text}`);
|
|
|
|
if (text.includes('EADDRINUSE')) {
|
|
const match = text.match(/:(\d+)/);
|
|
const port = match ? match[1] : null;
|
|
if (port) {
|
|
try {
|
|
if (process.platform === 'win32') {
|
|
require('child_process').spawnSync('powershell.exe', [
|
|
'-NoProfile',
|
|
'-NonInteractive',
|
|
'-Command',
|
|
`Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force }`
|
|
], { stdio: 'ignore', windowsHide: true });
|
|
} else {
|
|
require('child_process').spawnSync('fuser', ['-k', '-9', `${port}/tcp`], { stdio: 'ignore' });
|
|
}
|
|
} catch (_) { }
|
|
}
|
|
}
|
|
});
|
|
|
|
child.on('exit', (code, signal) => {
|
|
this.logToFile(name, `EXITED: code=${code} signal=${signal}`);
|
|
this.processes[name] = null;
|
|
|
|
if (this.stableTimers[name]) {
|
|
clearTimeout(this.stableTimers[name]);
|
|
this.stableTimers[name] = null;
|
|
}
|
|
|
|
if (!this.isStopping) {
|
|
this.restartCounts[name] = (this.restartCounts[name] || 0) + 1;
|
|
if (this.restartCounts[name] > 5) {
|
|
this.logToFile(name, `Crashed too many times, giving up.`);
|
|
return;
|
|
}
|
|
setTimeout(() => {
|
|
this.startService(name, relativeDir, startScript).catch(err => {
|
|
this.logToFile(name, `Failed to restart: ${err.message}`);
|
|
});
|
|
}, 1500);
|
|
}
|
|
});
|
|
}
|
|
|
|
async startServices(services) {
|
|
await Promise.all(services.map(({ name, relativeDir, startScript }) =>
|
|
this.startService(name, relativeDir, startScript)
|
|
));
|
|
}
|
|
|
|
async stopAll() {
|
|
this.isStopping = true;
|
|
for (const name of Object.keys(this.stableTimers)) {
|
|
if (this.stableTimers[name]) {
|
|
clearTimeout(this.stableTimers[name]);
|
|
}
|
|
}
|
|
this.stableTimers = {};
|
|
const killPromises = Object.keys(this.processes).map((name) => {
|
|
const procInfo = this.processes[name];
|
|
if (procInfo && procInfo.child) {
|
|
this.logToFile(name, `Killing service`);
|
|
return new Promise((resolve) => {
|
|
procInfo.child.removeAllListeners('exit');
|
|
procInfo.child.on('exit', () => resolve());
|
|
procInfo.child.kill();
|
|
});
|
|
}
|
|
return Promise.resolve();
|
|
});
|
|
|
|
await Promise.all(killPromises);
|
|
this.processes = {};
|
|
}
|
|
|
|
async waitForPorts(ports, timeoutMs = 30000) {
|
|
await Promise.all(ports.map((port) => this.waitForPort(port, timeoutMs)));
|
|
}
|
|
|
|
async waitForPort(port, timeoutMs = 30000) {
|
|
const start = Date.now();
|
|
const net = require('net');
|
|
|
|
while (Date.now() - start < timeoutMs) {
|
|
if (this.isStopping) return false;
|
|
const connected = await new Promise((resolve) => {
|
|
const socket = new net.Socket();
|
|
socket.setTimeout(1000);
|
|
socket.on('connect', () => {
|
|
socket.destroy();
|
|
resolve(true);
|
|
});
|
|
socket.on('error', () => {
|
|
resolve(false);
|
|
});
|
|
socket.on('timeout', () => {
|
|
socket.destroy();
|
|
resolve(false);
|
|
});
|
|
socket.connect(port, '127.0.0.1');
|
|
});
|
|
|
|
if (connected) return true;
|
|
await new Promise(r => setTimeout(r, 40));
|
|
}
|
|
throw new Error(`Timeout waiting for port ${port}`);
|
|
}
|
|
}
|
|
|
|
module.exports = ServiceManager; |