Files
minecraft_launcher/app/assets/js/serverruntime.js
claude-bot 9786cfe031
Some checks failed
Build / release (macos-latest) (push) Has been cancelled
Build / release (ubuntu-latest) (push) Has been cancelled
Build / release (windows-latest) (push) Has been cancelled
Windows Smoke Test / windows-smoke (push) Has been cancelled
Refactor launcher profiles and port automation
2026-05-05 21:52:17 +09:00

220 lines
6.1 KiB
JavaScript

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
}