const childProcess = require('child_process') const fs = require('fs-extra') const path = require('path') const ConfigManager = require('./configmanager') const PortManager = require('./portmanager') const ProfileAssetManager = require('./profileassetmanager') const runtimes = new Map() const READY_PATTERNS = [ /Done \([0-9.]+s\)!/i, /For help, type "help"/i ] function getRuntime(profileId){ if(!runtimes.has(profileId)){ runtimes.set(profileId, { serverProcess: null, logs: [], status: 'stopped', ready: false, readyPromise: null, readyResolver: null, readyRejecter: null }) } return runtimes.get(profileId) } function appendLog(runtime, line){ runtime.logs.push(line) if(runtime.logs.length > 200){ runtime.logs.shift() } } function createReadyPromise(runtime){ runtime.ready = false runtime.readyPromise = new Promise((resolve, reject) => { runtime.readyResolver = resolve runtime.readyRejecter = reject }) return runtime.readyPromise } function resolveReady(runtime){ if(runtime.ready === true){ return } runtime.ready = true runtime.status = 'running' if(runtime.readyResolver){ runtime.readyResolver(runtime) } runtime.readyResolver = null runtime.readyRejecter = null } function rejectReady(runtime, error){ runtime.ready = false if(runtime.readyRejecter){ runtime.readyRejecter(error) } runtime.readyResolver = null runtime.readyRejecter = null } function resolveServerJavaExecutable(){ const selectedServerId = ConfigManager.getSelectedServer() if(selectedServerId != null){ const configured = ConfigManager.getJavaExecutable(selectedServerId) if(configured){ return configured } } return 'java' } async function writeServerFiles(profile, serverDirectory){ await fs.writeFile(path.join(serverDirectory, 'eula.txt'), 'eula=true\n', 'utf8') const properties = [ ['enable-query', 'false'], ['max-players', String(profile.serverMaxPlayers ?? 20)], ['online-mode', 'true'], ['server-port', String(profile.serverPort ?? 25565)], ['white-list', profile.serverWhitelistEnabled ? 'true' : 'false'], ['level-name', 'world'] ] await fs.writeFile( path.join(serverDirectory, 'server.properties'), properties.map(([key, value]) => `${key}=${value}`).join('\n') + '\n', 'utf8' ) } function buildLaunchCommand(profile, javaExecutable){ const memoryMb = Number.isFinite(Number(profile.serverMemoryMb)) ? Math.max(512, Number(profile.serverMemoryMb)) : 4096 return `"${javaExecutable}" -Xms${memoryMb}M -Xmx${memoryMb}M -jar server.jar nogui` } exports.startHostedProfile = async function(profile){ if(profile.serverEnabled !== true){ throw new Error('서버 사용이 꺼진 프로필입니다.') } const runtime = getRuntime(profile.id) if(runtime.serverProcess != null){ if(runtime.readyPromise != null){ await runtime.readyPromise.catch(() => {}) } return runtime } const installedServer = await ProfileAssetManager.ensureServerJarInstalled(profile) if(installedServer?.serverJarPath == null){ throw new Error('로컬 서버를 시작하려면 버킷 JAR 업로드가 필요합니다.') } await ProfileAssetManager.ensureServerWorldInstalled(profile, installedServer.serverDirectory) await writeServerFiles(profile, installedServer.serverDirectory) await PortManager.ensurePortAvailability(profile) const command = buildLaunchCommand(profile, resolveServerJavaExecutable()) appendLog(runtime, `[launcher] starting server: ${command}`) runtime.status = 'starting' createReadyPromise(runtime) const serverProcess = childProcess.spawn(command, { cwd: installedServer.serverDirectory, shell: true, detached: false }) runtime.serverProcess = serverProcess const readyFallbackTimer = setTimeout(() => { if(runtime.serverProcess != null){ resolveReady(runtime) } }, 25000) serverProcess.stdout?.on('data', (chunk) => { chunk.toString().split(/\r?\n/).filter(Boolean).forEach((line) => { appendLog(runtime, `[server] ${line}`) if(READY_PATTERNS.some((pattern) => pattern.test(line))){ resolveReady(runtime) } }) }) serverProcess.stderr?.on('data', (chunk) => { chunk.toString().split(/\r?\n/).filter(Boolean).forEach((line) => { appendLog(runtime, `[server:err] ${line}`) }) }) serverProcess.on('close', () => { clearTimeout(readyFallbackTimer) runtime.serverProcess = null runtime.status = 'stopped' runtime.ready = false rejectReady(runtime, new Error('서버 프로세스가 종료되었습니다.')) PortManager.releaseProfilePort(profile.id).catch(() => {}) }) await runtime.readyPromise.catch(() => {}) return runtime } exports.stopHostedProfile = async function(profileId){ const runtime = getRuntime(profileId) if(runtime.serverProcess != null){ runtime.status = 'stopping' try { runtime.serverProcess.stdin?.write('stop\n') } catch (error) { void error } setTimeout(() => { if(runtime.serverProcess != null){ runtime.serverProcess.kill() } }, 5000) } runtime.status = 'stopped' runtime.ready = false await PortManager.releaseProfilePort(profileId).catch(() => {}) } exports.getHostedProfileState = function(profileId){ const runtime = getRuntime(profileId) return { status: runtime.status, running: runtime.serverProcess != null, ready: runtime.ready, logs: [...runtime.logs] } } exports.hasRunningProfiles = function(){ for(const runtime of runtimes.values()){ if(runtime.serverProcess != null){ return true } } return false }