minecraft-bedrock-server
Version:
minecraft bedrock server wrapper
287 lines (257 loc) • 9.42 kB
JavaScript
const http = require('https')
const fs = require('fs')
const cp = require('child_process')
const debug = process.env.CI ? console.debug : require('debug')('minecraft-bedrock-server')
const https = require('https')
const helpers = require('./helper')
const serversHtmlURL = 'https://www.minecraft.net/en-us/download/server/bedrock'
function head (url) {
return new Promise((resolve, reject) => {
const req = http.request(url, { method: 'HEAD', timeout: 1000 }, resolve)
req.on('error', reject)
req.on('timeout', () => { req.destroy(); debug('HEAD request timeout'); reject(new Error('timeout')) })
req.end()
})
}
function get (url, outPath) {
const file = fs.createWriteStream(outPath)
return new Promise((resolve, reject) => {
https.get(url, { timeout: 1000 * 20 }, response => {
if (response.statusCode !== 200) return reject(new Error('Server returned code ' + response.statusCode))
response.pipe(file)
file.on('finish', () => {
file.close()
resolve()
})
})
})
}
async function getLatestVersions () {
const html = await fetch(serversHtmlURL).then(res => res.text())
// Find ' <a href="https://minecraft.azureedge.net/bin-linux/bedrock-server-1.20.72.01.zip" aria-label="Download Minecraft Dedicated Server software for Ubuntu (Linux)" class="btn btn-disabled-outline mt-4 downloadlink" role="button" data-platform="serverBedrockLinux" tabindex="0" aria-disabled="true">Download </a>'
const links = [...html.matchAll(/a href="(.*?)" /g)].map(match => match[1])
function forOS (os) {
const url = links.find(link => link.includes(os + '/'))
if (!url) return null
const version4 = url.match(/bedrock-server-(\d+\.\d+\.\d+\.\d+)\.zip/)[1]
const version3 = version4.split('.').slice(0, 3).join('.')
return { version4, version3, url }
}
return {
linux: forOS('linux'),
windows: forOS('win'),
macos: forOS('osx'),
preview: {
linux: forOS('linux-preview'),
windows: forOS('win-preview'),
macos: forOS('osx-preview')
}
}
}
let downloadLock = false
// Download + extract vanilla server and enter the directory
async function download (os, version, root, path) {
if (downloadLock) {
throw Error('Already downloading server')
}
downloadLock = true
process.chdir(root)
if (version.split('.').length < 3) {
throw new Error('minecraft-bedrock-server: A version string should contain at least 3 dots on Minecraft Bedrock Edition. Please add a .0 suffix: ' + version)
}
const verStr = version.split('.').slice(0, 3).join('.')
const dir = path || 'bds-' + version
if (fs.existsSync(dir) && fs.readdirSync(dir).length > 1) {
process.chdir(dir) // Enter server folder
debug('Already downloaded', version)
downloadLock = false
return { version: verStr, path: process.cwd() }
}
try { fs.mkdirSync(dir) } catch { }
process.chdir(dir) // Enter server folder
const url = (os, version) => `https://www.minecraft.net/bedrockdedicatedserver/bin-${os}/bedrock-server-${version}.zip`
let found = false
for (let i = 0; i < 20; i++) { // Check for the latest server build for version (major.minor.patch.BUILD)
const u = url(os, `${verStr}.${String(i).padStart(2, '0')}`)
debug('Opening', u, Date.now())
let ret
try { ret = await head(u) } catch (e) { continue }
if (ret.statusCode === 200) {
found = u
debug('Found server', ret.statusCode)
break
}
}
if (!found) throw Error('did not find server bin for ' + os + ' ' + version)
console.info('🔻 Downloading', found)
await get(found, 'bds.zip')
console.info('⚡ Unzipping')
// Unzip server
if (process.platform === 'linux') cp.execSync('unzip -u bds.zip')
else cp.execSync('tar -xf bds.zip')
downloadLock = false
return { version: verStr, path: process.cwd() }
}
function eraseServer (version, options) {
downloadLock = false
// Remove the server and try again
const currentDir = process.cwd()
process.chdir(options.root || '.')
const path = options.path ? options.path : 'bds-' + version
debug('Removing server', path)
fs.rmSync(path, { recursive: true, force: true })
process.chdir(currentDir)
}
const defaultOptions = {
'level-generator': '2',
'server-port': '19130',
'online-mode': 'false'
}
const internalOptions = ['path', 'root']
// Setup the server
function configure (options = {}) {
const opts = { ...defaultOptions, ...options }
let config = fs.readFileSync('./server.properties', 'utf-8')
config = config.split('## node options')[0].trim()
config += '\n## node options'
config += '\nplayer-idle-timeout=1\nallow-cheats=true\ndefault-player-permission-level=operator'
for (const o in opts) {
if (internalOptions.includes(o)) continue
config += `\n${o}=${opts[o]}`
}
fs.writeFileSync('./server.properties', config)
if (process.platform === 'linux') {
cp.execSync('chmod +777 ./bedrock_server')
}
}
function run (inheritStdout = true) {
const exe = process.platform === 'win32' ? 'bedrock_server.exe' : './bedrock_server'
return cp.spawn(exe, inheritStdout ? { stdio: 'inherit' } : {})
}
async function downloadServer (version, options) {
const platFix = {
win32: 'win',
windows: 'win',
linux: 'linux',
macos: 'darwin'
}
if (options.platform && !platFix[options.platform]) {
throw new Error('Unsupported specified platform: ' + options.platform)
}
const platform = options.platform || process.platform
const serverOs = platFix[platform] || 'linux'
const currentDir = process.cwd()
try {
const ret = await download(serverOs, version, options.root || '.', options.path)
return ret
} finally {
process.chdir(currentDir)
downloadLock = false
}
}
let lastHandle
// Run the server
async function startServer (version, onStart, options = {}) {
const os = process.platform === 'win32' ? 'win' : process.platform
if (os !== 'win' && os !== 'linux') {
throw Error('unsupported os ' + os)
}
const currentDir = process.cwd()
// Take the options.path and determine if it's an absolute path or not
const path = options.path
const pathRoot = options.root || '.'
let ver
try {
ver = await download(os, version, pathRoot, path) // and enter the directory
} finally {
downloadLock = false
}
debug('Configuring server', ver.version)
configure(options)
debug('Starting server', ver.version)
const handle = lastHandle = run(!onStart)
handle.on('error', (...a) => {
console.warn('*** THE MINECRAFT PROCESS CRASHED ***', a)
handle.kill('SIGKILL')
})
if (onStart) {
let stdout = ''
function processLine (data) {
stdout += data
if (stdout.includes('Server started')) {
onStart()
handle.stdout.off('data', processLine)
}
}
handle.stdout.on('data', processLine)
handle.stdout.pipe(process.stdout)
handle.stderr.pipe(process.stdout)
}
process.chdir(currentDir)
return handle
}
// Start the server and wait for it to be ready, with a timeout
function startServerAndWait (version, withTimeout, options) {
if (isNaN(withTimeout)) throw Error('timeout must be a number')
let handle
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
handle?.kill()
reject(new Error(`Server did not start on time (${withTimeout}ms, now ${Date.now()})`))
}, withTimeout)
startServer(version, function onReady () {
clearTimeout(timeout)
resolve(handle)
}, options).then((h) => {
handle = h
}).catch(reject)
})
}
// Start the server and wait for it to be ready, with a timeout, and retry once
async function startServerAndWait2 (version, withTimeout, options) {
const currentDir = process.cwd()
try {
return await startServerAndWait(version, withTimeout, options)
} catch (e) {
console.log(e)
console.log('^ Trying once more to start server in 10 seconds...')
lastHandle?.kill()
await new Promise(resolve => setTimeout(resolve, 10000))
process.chdir(currentDir) // We can't call eraseServer being inside the server directory
await eraseServer(version, options)
return await startServerAndWait(version, withTimeout, options)
}
}
class BedrockVanillaServer {
constructor (path, version, options) {
this.path = path || '.'
this.version = version
this.options = options
helpers.injectServerHelpers(this)
}
async startAndWaitReady (timeout = 1000 * 60 * 5) {
this.activeHandle = await startServerAndWait(this.version, timeout, this.options)
this.activeHandle.stop = () => this.stop()
this.activeHandle.on('exit', () => { this.activeHandle = null })
return this.activeHandle
}
async stop () {
return new Promise((resolve) => {
if (this.activeHandle) {
this.activeHandle.on('exit', resolve)
this.activeHandle.stdin.write('stop\n')
setTimeout(() => {
this.activeHandle.kill()
this.activeHandle = null
}, 1000)
} else {
resolve()
}
})
}
}
async function prepare (version, options) {
const dl = await downloadServer(version, options || {})
return new BedrockVanillaServer(dl.path, dl.version, options || {})
}
module.exports = { getLatestVersions, downloadServer, startServer, startServerAndWait, startServerAndWait2, prepare }