212 lines
6.3 KiB
JavaScript
212 lines
6.3 KiB
JavaScript
const childProcess = require('child_process')
|
|
const fs = require('fs-extra')
|
|
const path = require('path')
|
|
|
|
const ConfigManager = require('./configmanager')
|
|
const ProfileAssetManager = require('./profileassetmanager')
|
|
|
|
const runtimes = new Map()
|
|
|
|
function getRuntime(profileId){
|
|
if(!runtimes.has(profileId)){
|
|
runtimes.set(profileId, {
|
|
serverProcess: null,
|
|
tunnelProcess: null,
|
|
logs: [],
|
|
status: 'stopped',
|
|
publishedAddress: ConfigManager.getPublishedLibraryServerAddress(profileId)
|
|
})
|
|
}
|
|
|
|
return runtimes.get(profileId)
|
|
}
|
|
|
|
function appendLog(runtime, line){
|
|
runtime.logs.push(line)
|
|
if(runtime.logs.length > 200){
|
|
runtime.logs.shift()
|
|
}
|
|
}
|
|
|
|
function interpolateCommand(template, variables){
|
|
return Object.entries(variables).reduce((command, [key, value]) => {
|
|
return command.replaceAll(`\${${key}}`, String(value))
|
|
}, template)
|
|
}
|
|
|
|
function extractPublishedAddress(line, profile){
|
|
if(profile.tunnelAddressRegex){
|
|
const customRegex = new RegExp(profile.tunnelAddressRegex)
|
|
const customMatch = line.match(customRegex)
|
|
if(customMatch){
|
|
return customMatch[1] ?? customMatch[0]
|
|
}
|
|
}
|
|
|
|
const genericMatch = line.match(/([a-zA-Z0-9.-]+\.[a-zA-Z]{2,}:\d+|[a-zA-Z0-9.-]+:\d{2,5})/)
|
|
return genericMatch ? genericMatch[1] : null
|
|
}
|
|
|
|
async function resolveServerLaunchCommand(profile, serverDirectory){
|
|
if(profile.serverLaunchCommand){
|
|
return profile.serverLaunchCommand
|
|
}
|
|
|
|
const startScript = process.platform === 'win32' ? 'start.bat' : 'start.sh'
|
|
const startScriptPath = path.join(serverDirectory, startScript)
|
|
if(await fs.pathExists(startScriptPath)){
|
|
return process.platform === 'win32' ? startScript : `./${startScript}`
|
|
}
|
|
|
|
const jarPath = path.join(serverDirectory, 'server.jar')
|
|
if(await fs.pathExists(jarPath)){
|
|
return 'java -jar server.jar nogui'
|
|
}
|
|
|
|
throw new Error('서버 시작 명령을 결정할 수 없습니다. serverLaunchCommand 또는 server.jar/start 스크립트를 준비하세요.')
|
|
}
|
|
|
|
function resolveWorkingDirectory(profile, serverDirectory){
|
|
if(profile.serverWorkingDirectory){
|
|
return path.join(serverDirectory, profile.serverWorkingDirectory)
|
|
}
|
|
return serverDirectory
|
|
}
|
|
|
|
async function startTunnelProcess(profile, runtime, serverDirectory){
|
|
if(!profile.tunnelCommand){
|
|
return null
|
|
}
|
|
|
|
const command = interpolateCommand(profile.tunnelCommand, {
|
|
port: profile.serverPort ?? 25565,
|
|
serverDir: serverDirectory
|
|
})
|
|
|
|
const tunnelProcess = childProcess.spawn(command, {
|
|
cwd: serverDirectory,
|
|
shell: true,
|
|
detached: false
|
|
})
|
|
|
|
runtime.tunnelProcess = tunnelProcess
|
|
|
|
tunnelProcess.stdout?.on('data', (chunk) => {
|
|
const text = chunk.toString()
|
|
text.split(/\r?\n/).filter(Boolean).forEach((line) => {
|
|
appendLog(runtime, `[tunnel] ${line}`)
|
|
const address = extractPublishedAddress(line, profile)
|
|
if(address){
|
|
runtime.publishedAddress = address
|
|
ConfigManager.setPublishedLibraryServerAddress(profile.id, address)
|
|
ConfigManager.save()
|
|
}
|
|
})
|
|
})
|
|
|
|
tunnelProcess.stderr?.on('data', (chunk) => {
|
|
chunk.toString().split(/\r?\n/).filter(Boolean).forEach((line) => {
|
|
appendLog(runtime, `[tunnel:err] ${line}`)
|
|
})
|
|
})
|
|
|
|
tunnelProcess.on('close', () => {
|
|
runtime.tunnelProcess = null
|
|
})
|
|
|
|
return tunnelProcess
|
|
}
|
|
|
|
exports.startHostedProfile = async function(profile){
|
|
const runtime = getRuntime(profile.id)
|
|
if(runtime.serverProcess != null){
|
|
return runtime
|
|
}
|
|
|
|
const serverDirectory = await ProfileAssetManager.ensureServerBundleInstalled(profile)
|
|
const workingDirectory = resolveWorkingDirectory(profile, serverDirectory)
|
|
const command = await resolveServerLaunchCommand(profile, workingDirectory)
|
|
|
|
runtime.status = 'starting'
|
|
runtime.publishedAddress = null
|
|
ConfigManager.setPublishedLibraryServerAddress(profile.id, null)
|
|
ConfigManager.save()
|
|
appendLog(runtime, `[launcher] starting server: ${command}`)
|
|
|
|
const serverProcess = childProcess.spawn(command, {
|
|
cwd: workingDirectory,
|
|
shell: true,
|
|
detached: false
|
|
})
|
|
|
|
runtime.serverProcess = serverProcess
|
|
|
|
serverProcess.stdout?.on('data', (chunk) => {
|
|
chunk.toString().split(/\r?\n/).filter(Boolean).forEach((line) => {
|
|
appendLog(runtime, `[server] ${line}`)
|
|
if(runtime.status !== 'running'){
|
|
runtime.status = 'running'
|
|
}
|
|
})
|
|
})
|
|
|
|
serverProcess.stderr?.on('data', (chunk) => {
|
|
chunk.toString().split(/\r?\n/).filter(Boolean).forEach((line) => {
|
|
appendLog(runtime, `[server:err] ${line}`)
|
|
})
|
|
})
|
|
|
|
serverProcess.on('close', () => {
|
|
runtime.serverProcess = null
|
|
runtime.status = 'stopped'
|
|
runtime.publishedAddress = null
|
|
ConfigManager.setPublishedLibraryServerAddress(profile.id, null)
|
|
ConfigManager.save()
|
|
})
|
|
|
|
if(profile.tunnelCommand){
|
|
await startTunnelProcess(profile, runtime, workingDirectory)
|
|
}
|
|
|
|
return runtime
|
|
}
|
|
|
|
exports.stopHostedProfile = function(profileId){
|
|
const runtime = getRuntime(profileId)
|
|
|
|
if(runtime.tunnelProcess != null){
|
|
runtime.tunnelProcess.kill()
|
|
runtime.tunnelProcess = null
|
|
}
|
|
|
|
if(runtime.serverProcess != null){
|
|
runtime.serverProcess.kill()
|
|
runtime.serverProcess = null
|
|
}
|
|
|
|
runtime.status = 'stopped'
|
|
runtime.publishedAddress = null
|
|
ConfigManager.setPublishedLibraryServerAddress(profileId, null)
|
|
ConfigManager.save()
|
|
}
|
|
|
|
exports.getHostedProfileState = function(profileId){
|
|
const runtime = getRuntime(profileId)
|
|
return {
|
|
status: runtime.status,
|
|
running: runtime.serverProcess != null,
|
|
tunneling: runtime.tunnelProcess != null,
|
|
publishedAddress: runtime.publishedAddress ?? ConfigManager.getPublishedLibraryServerAddress(profileId),
|
|
logs: [...runtime.logs]
|
|
}
|
|
}
|
|
|
|
exports.hasRunningProfiles = function(){
|
|
for(const runtime of runtimes.values()){
|
|
if(runtime.serverProcess != null || runtime.tunnelProcess != null){
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|