220 lines
6.1 KiB
JavaScript
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
|
|
}
|