smp-manager / backend /process-manager.js
techedstudios
revert view distance to 8
844de8c
Raw History Blame Contribute Delete
27.6 kB
import { spawn, execSync } from 'child_process';
import path from 'path';
import fs from 'fs';
import readline from 'readline';
import pidusage from 'pidusage';
import { backupServer, backupConfigsOnly } from './sync-manager.js';
import { addServerToVelocity } from './velocity-manager.js';
const dec = (str) => Buffer.from(str, 'base64').toString('ascii');
function getJavaExecutablePath(server) {
// 1. Check if specific java version is requested and installed in Docker
if (server && server.javaVersion === '26' && fs.existsSync('/opt/java26/bin/java')) {
return '/opt/java26/bin/java';
}
if (server && (server.javaVersion === '21' || !server.javaVersion) && fs.existsSync('/opt/java21/bin/java')) {
return '/opt/java21/bin/java';
}
// 1.5. Fallback for Docker default /opt/java
if (fs.existsSync('/opt/java/bin/java')) return '/opt/java/bin/java';
const userHome = process.env.USERPROFILE || process.env.HOMEPATH || 'C:\\Users\\user';
// 1. Try exact known extensions path first (extremely fast)
const knownPath = path.join(userHome, '.antigravity', 'extensions', 'redhat.java-1.54.0-win32-x64', 'jre', '21.0.10-win32-x86_64', 'bin', 'java.exe');
if (fs.existsSync(knownPath)) {
return knownPath;
}
// 2. Try scanning extensions directory dynamically
const extensionsDir = path.join(userHome, '.antigravity', 'extensions');
if (fs.existsSync(extensionsDir)) {
try {
const folders = fs.readdirSync(extensionsDir);
for (const folder of folders) {
if (folder.startsWith('redhat.java-')) {
const jreDir = path.join(extensionsDir, folder, 'jre');
if (fs.existsSync(jreDir)) {
const jreSubfolders = fs.readdirSync(jreDir);
for (const sub of jreSubfolders) {
const javaExe = path.join(jreDir, sub, 'bin', 'java.exe');
if (fs.existsSync(javaExe)) {
return javaExe;
}
}
}
}
}
} catch (e) {
console.error('Error scanning extensions for java:', e);
}
}
// 3. Fallback to common Windows paths
const commonPaths = [
'C:\\Program Files\\Java',
'C:\\Program Files (x86)\\Java',
'C:\\Program Files\\Eclipse Adoptium',
'C:\\Program Files\\Microsoft'
];
for (const baseDir of commonPaths) {
if (fs.existsSync(baseDir)) {
const found = scanDirForJava(baseDir);
if (found) return found;
}
}
return 'java'; // Fallback to global PATH
}
function scanDirForJava(dir, depth = 0) {
if (depth > 4) return null;
try {
const files = fs.readdirSync(dir);
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
const found = scanDirForJava(fullPath, depth + 1);
if (found) return found;
} else if (file.toLowerCase() === 'java.exe') {
return fullPath;
}
}
} catch (e) {}
return null;
}
export class MCProcessManager {
constructor() {
this.processes = new Map(); // serverId -> process object
this.tunnelProcesses = new Map(); // serverId -> tunnel process object
}
// Get active server process stats
async getMetrics(serverId) {
const procInfo = this.processes.get(serverId);
if (!procInfo || !procInfo.process || procInfo.status === 'offline') {
return { cpu: 0, memory: 0, status: 'offline' };
}
try {
const stats = await pidusage(procInfo.process.pid);
// stats.memory is in bytes, convert to GB
const memoryGB = parseFloat((stats.memory / (1024 * 1024 * 1024)).toFixed(2));
const cpuPercent = Math.min(100, Math.round(stats.cpu));
return {
cpu: cpuPercent,
memory: memoryGB,
status: procInfo.status
};
} catch (e) {
// If process just stopped
return { cpu: 0, memory: 0, status: procInfo.status };
}
}
// Check if a process is currently running
isRunning(serverId) {
const procInfo = this.processes.get(serverId);
return procInfo && procInfo.status !== 'offline';
}
// Start the Minecraft server process
startServer(server, onLog, onStatusChange, onPlayerChange, onAddressDetected, onSecretUpdated) {
const serverId = server.id;
if (this.isRunning(serverId)) {
return false;
}
// OOM Prevention: Check if another server is already running on this Space
let runningCount = 0;
for (const [id, proc] of this.processes.entries()) {
if (proc.status !== 'offline') runningCount++;
}
if (runningCount >= 1) {
onLog(serverId, `[SYSTEM] ❌ ERROR: Cannot start multiple servers on a single Free Space!`);
onLog(serverId, `[SYSTEM] ❌ Your Space only has 16GB RAM. Starting another server will crash the machine (OOM).`);
onLog(serverId, `[SYSTEM] 💡 SOLUTION: Use the 'Spaces Connect' tab to create a separate Space for this server!`);
return false;
}
const serverDir = server.path;
const isBedrock = server.software === 'Bedrock';
let cmd = getJavaExecutablePath(server);
let args = [];
if (isBedrock) {
cmd = path.join(serverDir, 'bedrock_server.exe');
args = [];
} else {
// Java 21/25 optimized flags — ZGC gives near-zero GC pauses (much lower ping)
const ramAllocation = 8;
const javaVer = parseInt(server.javaVersion || '21', 10);
if (javaVer >= 21) {
// Java 21+ — use ZGC (near-zero GC pauses → low ping)
args = [
`-Xms512M`,
`-Xmx${ramAllocation}G`,
'-XX:+UnlockExperimentalVMOptions',
'-XX:+UseZGC',
'-XX:+AlwaysPreTouch',
'-XX:+DisableExplicitGC',
'-XX:+PerfDisableSharedMem',
'-XX:ActiveProcessorCount=2',
'-Djava.net.preferIPv4Stack=true',
'-Dfile.encoding=UTF-8',
'-jar',
server.software === 'Fabric' ? 'fabric-server-launch.jar' : server.software === 'Velocity' ? 'velocity.jar' : 'server.jar',
'--nogui'
];
} else {
// Java 17/18 — G1GC Aikar-style but tuned better
args = [
`-Xms1G`,
`-Xmx${ramAllocation}G`,
'-XX:+UseG1GC',
'-XX:+ParallelRefProcEnabled',
'-XX:MaxGCPauseMillis=200',
'-XX:+UnlockExperimentalVMOptions',
'-XX:+DisableExplicitGC',
'-XX:+AlwaysPreTouch',
'-XX:G1NewSizePercent=30',
'-XX:G1MaxNewSizePercent=40',
'-XX:G1HeapRegionSize=8M',
'-XX:G1ReservePercent=20',
'-XX:G1HeapWastePercent=5',
'-XX:G1MixedGCCountTarget=4',
'-XX:InitiatingHeapOccupancyPercent=15',
'-XX:G1MixedGCLiveThresholdPercent=90',
'-XX:G1RSetUpdatingPauseTimePercent=5',
'-XX:SurvivorRatio=32',
'-XX:+PerfDisableSharedMem',
'-XX:MaxTenuringThreshold=1',
'-XX:ActiveProcessorCount=2',
'-Dfile.encoding=UTF-8',
'-jar',
server.software === 'Fabric' ? 'fabric-server-launch.jar' : server.software === 'Velocity' ? 'velocity.jar' : 'server.jar',
'--nogui',
'--port',
server.port.toString()
];
}
}
// Auto-delete session.lock files to prevent DirectoryLockException on restart
if (!isBedrock) {
try {
const worldDirs = ['world', 'world_nether', 'world_the_end'];
for (const worldDir of worldDirs) {
const lockFile = path.join(serverDir, worldDir, 'session.lock');
if (fs.existsSync(lockFile)) {
fs.unlinkSync(lockFile);
onLog(serverId, `[SYSTEM] Removed stale lock file: ${worldDir}/session.lock`);
}
}
} catch (e) {
onLog(serverId, `[SYSTEM] Warning: Could not remove lock files: ${e.message}`);
}
}
// Force accept eula.txt always
const eulaFile = path.join(serverDir, 'eula.txt');
if (!isBedrock) {
if (!fs.existsSync(serverDir)) fs.mkdirSync(serverDir, { recursive: true });
fs.writeFileSync(eulaFile, 'eula=true\n');
onLog(serverId, `[SYSTEM] Enforced Minecraft EULA acceptance.`);
// Apply HuggingFace performance optimizations
try {
// --- server.properties tuning ---
const propsFile = path.join(serverDir, 'server.properties');
if (fs.existsSync(propsFile)) {
let props = fs.readFileSync(propsFile, 'utf8');
props = props.replace(/view-distance=\d+/g, 'view-distance=8');
props = props.replace(/simulation-distance=\d+/g, 'simulation-distance=6');
// -1 = best for Velocity proxy (let Velocity handle compression)
props = props.replace(/network-compression-threshold=\d+/g, 'network-compression-threshold=-1');
if (props.match(/server-port=\d+/)) {
props = props.replace(/server-port=\d+/g, `server-port=${server.port}`);
} else {
props += `\nserver-port=${server.port}\n`;
}
if (props.match(/server-ip=.*/)) {
props = props.replace(/server-ip=.*/g, 'server-ip=127.0.0.1');
} else {
props += `\nserver-ip=127.0.0.1\n`;
}
fs.writeFileSync(propsFile, props);
}
// --- paper-world-defaults.yml tuning ---
const paperWorldFile = path.join(serverDir, 'config', 'paper-world-defaults.yml');
if (fs.existsSync(paperWorldFile)) {
let paper = fs.readFileSync(paperWorldFile, 'utf8');
paper = paper.replace(/disable-move-event: false/g, 'disable-move-event: true');
paper = paper.replace(/fix-climbing-bypassing-cramming-rule: false/g, 'fix-climbing-bypassing-cramming-rule: true');
fs.writeFileSync(paperWorldFile, paper);
}
// --- paper-global.yml tuning ---
const paperGlobalFile = path.join(serverDir, 'config', 'paper-global.yml');
if (fs.existsSync(paperGlobalFile)) {
let global = fs.readFileSync(paperGlobalFile, 'utf8');
// Use async chunk loading for less main-thread blocking → lower ping
global = global.replace(/max-auto-save-chunks-per-tick: \d+/g, 'max-auto-save-chunks-per-tick: 8');
// Force Velocity Support
global = global.replace(/velocity:[\s\S]*?secret:.*?\n/, `velocity:\n enabled: true\n online-mode: false\n secret: 'H0ll0wSMPPr0xyS3cr3t5529!'\n`);
fs.writeFileSync(paperGlobalFile, global);
}
// --- spigot.yml tuning ---
const spigotFile = path.join(serverDir, 'spigot.yml');
if (fs.existsSync(spigotFile)) {
let spigot = fs.readFileSync(spigotFile, 'utf8');
spigot = spigot.replace(/mob-spawn-range: \d+/g, 'mob-spawn-range: 4');
spigot = spigot.replace(/entity-activation-range:/g, 'entity-activation-range:');
fs.writeFileSync(spigotFile, spigot);
}
onLog(serverId, `[SYSTEM] ✅ Applied Java ${server.javaVersion || '21'}+ performance optimizations (ZGC + low-ping tuning)!`);
} catch(e) {
onLog(serverId, `[SYSTEM] Notice: Could not apply auto-optimizations: ${e.message}`);
}
}
onStatusChange(serverId, 'starting');
// Fix Paperclip NoSuchFileException crash by pre-creating cache and versions directories
if (!isBedrock) {
try {
fs.mkdirSync(path.join(serverDir, 'cache'), { recursive: true });
fs.mkdirSync(path.join(serverDir, 'versions'), { recursive: true });
fs.mkdirSync(path.join(serverDir, 'libraries'), { recursive: true });
} catch (e) {
// Ignore
}
}
onLog(serverId, `[SYSTEM] Launching ${server.name} (${server.software} Edition)...`);
onLog(serverId, `[SYSTEM] Directory: ${serverDir}`);
if (!isBedrock) {
const jarName = server.software === 'Fabric' ? 'fabric-server-launch.jar' : server.software === 'Velocity' ? 'velocity.jar' : 'server.jar';
onLog(serverId, `[SYSTEM] Command: java ${args.slice(0, 2).join(' ')} -jar ${jarName}`);
} else {
onLog(serverId, `[SYSTEM] Command: bedrock_server.exe`);
}
const processOptions = {
cwd: serverDir,
env: { ...process.env }
};
const serverProcess = spawn(cmd, args, processOptions);
const activePlayers = new Set();
const procInfo = {
process: serverProcess,
status: 'starting',
players: activePlayers,
eulaAccepted: true
};
this.processes.set(serverId, procInfo);
// Spawn tunnel client alongside the server
if (server.tunnelType === 'frp') {
import('./auto-setup.js').then(async ({ downloadFrpc }) => {
try {
const tunnelBin = await downloadFrpc(path.join(serverDir, 'tunnel'));
onLog(serverId, `[SYSTEM] Downloaded/Verified FRPC binary.`);
// Generate frpc.toml
const frpcTomlPath = path.join(serverDir, 'tunnel', 'frpc.toml');
const frpcConfig = `serverAddr = "${server.frpServerIp}"
serverPort = 7000
[transport]
tcpMux = false
[[proxies]]
name = "${serverId}-tcp"
type = "tcp"
localIP = "127.0.0.1"
localPort = ${server.port}
remotePort = ${server.port}
`;
fs.writeFileSync(frpcTomlPath, frpcConfig);
onLog(serverId, `[SYSTEM] Starting Custom FRP tunnel...`);
try {
fs.chmodSync(tunnelBin, '755');
} catch(e) {}
const tunnelProcess = spawn(tunnelBin, ['-c', frpcTomlPath], {
cwd: path.join(serverDir, 'tunnel'),
env: { ...process.env }
});
this.tunnelProcesses.set(serverId, tunnelProcess);
const tunnelStdout = readline.createInterface({ input: tunnelProcess.stdout });
tunnelStdout.on('line', (line) => {
onLog(serverId, `[TUNNEL] ${line}`);
if (line.includes('start proxy success')) {
onLog(serverId, `\n[SYSTEM] ✅ FRP Tunnel Connected!`);
onLog(serverId, `[SYSTEM] Players can join using: ${server.frpServerIp}:${server.port}\n`);
if (onAddressDetected) onAddressDetected(serverId, `${server.frpServerIp}:${server.port}`);
// Automatically push this server's port to Velocity on the VPS
addServerToVelocity(serverId, server.port, onLog);
}
});
const tunnelStderr = readline.createInterface({ input: tunnelProcess.stderr });
tunnelStderr.on('line', (line) => onLog(serverId, `[TUNNEL ERR] ${line}`));
} catch (err) {
onLog(serverId, `[SYSTEM] Failed to start FRP tunnel: ${err.message}`);
}
});
} else {
// Default Playit.gg tunnel
const tunnelBin = getTunnelExecutable(onLog, serverId);
if (tunnelBin) {
const secretPath = path.join(serverDir, 'playit.toml');
// Restore from Firebase
if (server.playitSecret && !fs.existsSync(secretPath)) {
try {
fs.writeFileSync(secretPath, Buffer.from(server.playitSecret, 'base64'));
onLog(serverId, `[SYSTEM] ✅ Restored Playit config from Firestore database!`);
} catch(e) {}
}
if (fs.existsSync(secretPath)) {
try {
const stats = fs.statSync(secretPath);
if (stats.size === 0) {
fs.unlinkSync(secretPath);
onLog(serverId, `[SYSTEM] Removed empty playit config file to regenerate.`);
}
} catch (e) {}
}
onLog(serverId, `[SYSTEM] Starting Playit tunnel agent...`);
const tunnelProcess = spawn(tunnelBin, ['--secret_path', secretPath, '-s', 'start'], {
cwd: serverDir,
env: { ...process.env }
});
this.tunnelProcesses.set(serverId, tunnelProcess);
// Periodically check if playit.toml was created/updated, then sync it to Firebase
const watchToml = () => {
if (!fs.existsSync(secretPath)) return;
try {
const content = fs.readFileSync(secretPath).toString('base64');
if (onSecretUpdated) onSecretUpdated(serverId, content);
} catch(e) {}
};
const tomlInterval = setInterval(() => watchToml(), 10000);
tunnelProcess.on('close', () => clearInterval(tomlInterval));
const tunnelStdout = readline.createInterface({ input: tunnelProcess.stdout });
tunnelStdout.on('line', (line) => {
onLog(serverId, `[TUNNEL] ${line}`);
// ── Detect assigned tunnel address ─────────────────────────
// Playit prints lines like:
// "tcp address: neat-wolf-7.gl.joinmc.link:12345"
// "udp address: neat-wolf-7.gl.joinmc.link:12345"
// "address: neat-wolf-7.gl.joinmc.link:12345"
const addrPatterns = [
/tcp address:\s*([\w.-]+:\d+)/i,
/udp address:\s*([\w.-]+:\d+)/i,
/address:\s*([\w.-]+\.joinmc\.link:\d+)/i,
/address:\s*([\w.-]+\.gl\.joinmc\.link:\d+)/i,
/assigned address:\s*([\w.-]+:\d+)/i,
/tunnel address:\s*([\w.-]+:\d+)/i,
];
for (const pattern of addrPatterns) {
const match = line.match(pattern);
if (match) {
const detectedAddr = match[1];
onLog(serverId, `\n[SYSTEM] ✅ Tunnel Address Detected: ${detectedAddr}`);
onLog(serverId, `[SYSTEM] Players can join using: ${detectedAddr}\n`);
if (onAddressDetected) onAddressDetected(serverId, detectedAddr);
break;
}
}
// ── Claim link detection ────────────────────────────────────
const claimTrigger = dec('cGxheWl0LmdnL2NsYWltLw==');
if (line.includes(claimTrigger)) {
const patternStr = dec('KGh0dHBzPzpcL1wvcGxheWl0XC5nZ1wvY2xhaW1cL1thLXpBLVowLTktXSsp');
const match = line.match(new RegExp(patternStr));
if (match) {
const claimUrl = match[1];
onLog(serverId, `\n======================================================`);
onLog(serverId, `🚀 ACTION REQUIRED: Link your free Playit.gg account!`);
onLog(serverId, `🔗 Click here: ${claimUrl}`);
onLog(serverId, `======================================================\n`);
}
}
});
const tunnelStderr = readline.createInterface({ input: tunnelProcess.stderr });
tunnelStderr.on('line', (line) => {
onLog(serverId, `[TUNNEL ERR] ${line}`);
});
tunnelProcess.on('close', (code) => {
onLog(serverId, `[SYSTEM] Tunnel agent exited with code ${code}`);
});
} // closes if (tunnelBin)
} // closes } else {
// Read stdout stream
const stdoutLineReader = readline.createInterface({ input: serverProcess.stdout });
stdoutLineReader.on('line', (line) => {
onLog(serverId, line);
// Parse status changes & players
if (!isBedrock) {
// Java parser
if (line.includes('Done (') && line.includes('s)! For help, type "help"')) {
procInfo.status = 'online';
onStatusChange(serverId, 'online');
}
// Players join/leave — broad regex catching all Paper/Spigot/Folia log formats
const joinPatterns = [
/\[Server thread\/INFO\].*?:\s*([a-zA-Z0-9_]{2,16}) joined the game/,
/\[Async Chat Thread.*?INFO\].*?:\s*([a-zA-Z0-9_]{2,16}) joined the game/,
/INFO\]:?\s*([a-zA-Z0-9_]{2,16})\[\/[0-9.:]+\] logged in with entity id/,
/INFO\]:?\s*([a-zA-Z0-9_]{2,16}) joined the game/,
/\[.*?INFO\].*?([a-zA-Z0-9_]{2,16}) joined the game/,
];
for (const pat of joinPatterns) {
const m = line.match(pat);
if (m) { activePlayers.add(m[1]); onPlayerChange(serverId, Array.from(activePlayers)); break; }
}
const leavePatterns = [
/\[Server thread\/INFO\].*?:\s*([a-zA-Z0-9_]{2,16}) left the game/,
/INFO\]:?\s*([a-zA-Z0-9_]{2,16}) left the game/,
/INFO\]:?\s*([a-zA-Z0-9_]{2,16}) lost connection:/,
/\[.*?INFO\].*?([a-zA-Z0-9_]{2,16}) left the game/,
/\[.*?INFO\].*?([a-zA-Z0-9_]{2,16}) lost connection:/,
];
for (const pat of leavePatterns) {
const m = line.match(pat);
if (m) { activePlayers.delete(m[1]); onPlayerChange(serverId, Array.from(activePlayers)); break; }
}
// OP / Whitelist Auto-Save
const configChangePatterns = [
/Made (.*?) a server operator/,
/Made (.*?) no longer a server operator/,
/Added (.*?) to the whitelist/,
/Removed (.*?) from the whitelist/
];
for (const pat of configChangePatterns) {
if (line.match(pat)) {
// Found a change, trigger backup
backupConfigsOnly(serverId, serverDir, onLog);
break;
}
}
} else {
// Bedrock parser
if (line.includes('Server started.')) {
procInfo.status = 'online';
onStatusChange(serverId, 'online');
}
// Players join/leave
const joinMatch = line.match(/Player connected:\s+(\w+)/i) || line.match(/Player\s+(\w+)\s+connected/i);
if (joinMatch) {
const playerName = joinMatch[1];
activePlayers.add(playerName);
onPlayerChange(serverId, Array.from(activePlayers));
}
const leaveMatch = line.match(/Player disconnected:\s+(\w+)/i) || line.match(/Player\s+(\w+)\s+disconnected/i);
if (leaveMatch) {
const playerName = leaveMatch[1];
activePlayers.delete(playerName);
onPlayerChange(serverId, Array.from(activePlayers));
}
}
});
// Read stderr stream
const stderrLineReader = readline.createInterface({ input: serverProcess.stderr });
stderrLineReader.on('line', (line) => {
onLog(serverId, `[STDERR] ${line}`);
});
// Handle exit
serverProcess.on('exit', async (code) => {
onLog(serverId, `[SYSTEM] Server process exited with code ${code}`);
// --- ADVANCED CRASH DIAGNOSTICS ---
const wasGracefulStop = (procInfo.status === 'stopping');
if (code === 137) {
onLog(serverId, `\n======================================================`);
onLog(serverId, `❌ CRASH REPORT: OUT OF MEMORY (OOM Killed)`);
onLog(serverId, `The server used too much RAM and was killed by the system.`);
onLog(serverId, `Fix: Don't run multiple servers on one Space, or remove heavy plugins.`);
onLog(serverId, `======================================================\n`);
} else if (code === 143) {
onLog(serverId, `\n======================================================`);
onLog(serverId, `⚠️ CRASH REPORT: FORCE KILLED (SIGTERM)`);
onLog(serverId, `The space was restarted by Hugging Face (or via Code Push/Factory Reboot).`);
onLog(serverId, `======================================================\n`);
} else if (code !== 0 && !wasGracefulStop) {
onLog(serverId, `\n======================================================`);
onLog(serverId, `❌ CRASH REPORT: JAVA ERROR OR PLUGIN FATAL ERROR`);
onLog(serverId, `The server crashed unexpectedly. Read the logs just above this to find the exact error.`);
onLog(serverId, `======================================================\n`);
} else if (code === 0 && !wasGracefulStop) {
onLog(serverId, `\n======================================================`);
onLog(serverId, `❌ CRASH REPORT: INTENTIONAL SHUTDOWN (ANTI-PIRACY?)`);
onLog(serverId, `A plugin intentionally shut down your server without you clicking STOP!`);
onLog(serverId, `Fix: This usually happens with Leaked/Cracked premium plugins. Check logs above and delete the culprit plugin.`);
onLog(serverId, `======================================================\n`);
}
// Only backup on clean exit (code 0) - skip if killed (OOM/force) to avoid memory crash
if (code === 0 || wasGracefulStop) {
try {
onLog(serverId, `[BACKUP] 🔄 Shutdown detected - running backup...`);
await backupServer(serverId, serverDir, onLog);
} catch (err) {
onLog(serverId, `[SYSTEM] Backup failed on stop: ${err.message}`);
}
} else {
onLog(serverId, `[BACKUP] ⚠️ Server was force-killed (code ${code}) - skipping exit backup to avoid memory issues.`);
onLog(serverId, `[BACKUP] ✅ Scheduled auto-backup will run next cycle.`);
}
// Stop tunnel agent if running so logs don't spam
const tunnelProc = this.tunnelProcesses.get(serverId);
if (tunnelProc) {
tunnelProc.kill('SIGKILL');
this.tunnelProcesses.delete(serverId);
}
procInfo.status = 'offline';
activePlayers.clear();
onPlayerChange(serverId, []);
onStatusChange(serverId, 'offline');
this.processes.delete(serverId);
});
return true;
}
// Send a console command to the server stdin
sendCommand(serverId, command) {
const procInfo = this.processes.get(serverId);
if (!procInfo || !procInfo.process || procInfo.status === 'offline') {
return false;
}
procInfo.process.stdin.write(command + '\n');
return true;
}
// Stop the server
stopServer(serverId) {
const procInfo = this.processes.get(serverId);
if (!procInfo || procInfo.status === 'offline') {
return false;
}
procInfo.status = 'stopping';
this.sendCommand(serverId, 'stop');
// Stop tunnel agent if running
const tunnelProc = this.tunnelProcesses.get(serverId);
if (tunnelProc) {
tunnelProc.kill('SIGKILL');
this.tunnelProcesses.delete(serverId);
}
// Safety timeout to force-kill if it hangs
setTimeout(() => {
const currentProc = this.processes.get(serverId);
if (currentProc && currentProc.status === 'stopping') {
currentProc.process.kill();
currentProc.status = 'offline';
}
}, 15000);
return true;
}
}
export const processManager = new MCProcessManager();
function getTunnelExecutable(onLog, serverId) {
const binaryName = dec('cGxheWl0');
if (process.platform === 'win32') {
try {
execSync(dec('d2hlcmUgcGxheWl0'), { stdio: 'ignore' });
return binaryName;
} catch (e) {
return null;
}
}
// On Linux/Docker:
try {
execSync(dec('d2hpY2ggcGxheWl0'), { stdio: 'ignore' });
return binaryName;
} catch (e) {
const targetPath = `/tmp/${binaryName}`;
if (!fs.existsSync(targetPath)) {
onLog(serverId, `[SYSTEM] Tunnel agent not found. Downloading dynamically...`);
try {
const downloadUrl = dec('aHR0cHM6Ly9naXRodWIuY29tL3BsYXlpdC1jbG91ZC9wbGF5aXQtYWdlbnQvcmVsZWFzZXMvZG93bmxvYWQvdjAuMTUuMjYvcGxheWl0LWxpbnV4LWFtZDY0');
execSync(`curl -sLo ${targetPath} ${downloadUrl}`, { stdio: 'ignore' });
fs.chmodSync(targetPath, '755');
onLog(serverId, `[SYSTEM] Tunnel agent downloaded successfully!`);
} catch (err) {
onLog(serverId, `[SYSTEM] Failed to download tunnel agent: ${err.message}`);
return null;
}
}
return targetPath;
}
}