Vanilla server tests, client offline mode (#49)
* vanilla server launcher * update package.json * re-add babel to fix standard * fix ci * add buffer-equal * simple fixes * add offline client support * fix closing bugs, proper wait for server start * add test to mocha * change test timeout to 2 min * increase timeouts Co-authored-by: Romain Beaumont <romain.rom1@gmail.com>
This commit is contained in:
parent
bd97a8e1b7
commit
458136d877
13 changed files with 289 additions and 60 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -9,4 +9,5 @@ data/**/read.js
|
|||
data/**/write.js
|
||||
data/**/size.js
|
||||
samples/*.txt
|
||||
samples/*.json
|
||||
samples/*.json
|
||||
tools/bds*
|
||||
|
|
@ -15,7 +15,7 @@ function loadVersions () {
|
|||
} catch {}
|
||||
for (const file of files) {
|
||||
const rfile = file.replace(join(__dirname, '/', version) + '/', '')
|
||||
fileMap[rfile] ??= []
|
||||
fileMap[rfile] = fileMap[rfile] ?? []
|
||||
fileMap[rfile].push([Versions[version], file])
|
||||
fileMap[rfile].sort().reverse()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"test": "mocha",
|
||||
"pretest": "npm run lint",
|
||||
"lint": "standard",
|
||||
"vanillaServer": "node tools/startVanillaServer.js",
|
||||
"fix": "standard --fix"
|
||||
},
|
||||
"keywords": [
|
||||
|
|
@ -29,15 +30,18 @@
|
|||
"jsonwebtoken": "^8.5.1",
|
||||
"jsp-raknet": "github:extremeheat/raknet#client",
|
||||
"minecraft-folder-path": "^1.1.0",
|
||||
"node-fetch": "^2.6.1",
|
||||
"prismarine-nbt": "^1.5.0",
|
||||
"protodef": "github:extremeheat/node-protodef#compiler2",
|
||||
"protodef": "^1.11.0",
|
||||
"raknet-native": "^0.1.0",
|
||||
"uuid-1345": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.13.10",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"buffer-equal": "^1.0.0",
|
||||
"mocha": "^8.3.2",
|
||||
"protodef-yaml": "^1.0.1",
|
||||
"standard": "^16.0.3"
|
||||
},
|
||||
"standard": {
|
||||
|
|
|
|||
|
|
@ -7,16 +7,30 @@ const curve = 'secp384r1'
|
|||
module.exports = (client, server, options) => {
|
||||
const skinGeom = fs.readFileSync(DataProvider(options.protocolVersion).getPath('skin_geom.txt'), 'utf-8')
|
||||
|
||||
client.createClientChain = (mojangKey) => {
|
||||
client.createClientChain = (mojangKey, offline) => {
|
||||
mojangKey = mojangKey || require('./constants').PUBLIC_KEY
|
||||
const alice = client.ecdhKeyPair
|
||||
const alicePEM = ecPem(alice, curve) // https://github.com/nodejs/node/issues/15116#issuecomment-384790125
|
||||
const alicePEMPrivate = alicePEM.encodePrivateKey()
|
||||
|
||||
const token = JWT.sign({
|
||||
identityPublicKey: mojangKey,
|
||||
certificateAuthority: true
|
||||
}, alicePEMPrivate, { algorithm: 'ES384', header: { x5u: client.clientX509 } })
|
||||
let token
|
||||
if (offline) {
|
||||
const payload = {
|
||||
extraData: {
|
||||
displayName: client.username,
|
||||
identity: client.profile.uuid,
|
||||
titleId: '89692877'
|
||||
},
|
||||
certificateAuthority: true,
|
||||
identityPublicKey: client.clientX509
|
||||
}
|
||||
token = JWT.sign(payload, alicePEMPrivate, { algorithm: 'ES384', notBefore: 0, issuer: 'self', expiresIn: 60 * 60, header: { x5u: client.clientX509 } })
|
||||
} else {
|
||||
token = JWT.sign({
|
||||
identityPublicKey: mojangKey,
|
||||
certificateAuthority: true
|
||||
}, alicePEMPrivate, { algorithm: 'ES384', header: { x5u: client.clientX509 } })
|
||||
}
|
||||
|
||||
client.clientIdentityChain = token
|
||||
client.createClientUserChain(alicePEMPrivate)
|
||||
|
|
@ -24,46 +38,42 @@ module.exports = (client, server, options) => {
|
|||
|
||||
client.createClientUserChain = (privateKey) => {
|
||||
let payload = {
|
||||
ServerAddress: options.hostname,
|
||||
ThirdPartyName: client.profile.name,
|
||||
DeviceOS: client.session?.deviceOS || 1,
|
||||
GameVersion: options.version || '1.16.201',
|
||||
ClientRandomId: Date.now(), // TODO make biggeer
|
||||
DeviceId: '2099de18-429a-465a-a49b-fc4710a17bb3', // TODO random
|
||||
LanguageCode: 'en_GB', // TODO locale
|
||||
AnimatedImageData: [],
|
||||
PersonaPieces: [],
|
||||
PieceTintColours: [],
|
||||
SelfSignedId: '78eb38a6-950e-3ab9-b2cf-dd849e343701',
|
||||
SkinId: '5eb65f73-af11-448e-82aa-1b7b165316ad.persona-e199672a8c1a87e0-0',
|
||||
SkinData: 'AAAAAA==',
|
||||
SkinResourcePatch: 'ewogICAiZ2VvbWV0cnkiIDogewogICAgICAiYW5pbWF0ZWRfMTI4eDEyOCIgOiAiZ2VvbWV0cnkuYW5pbWF0ZWRfMTI4eDEyOF9wZXJzb25hLWUxOTk2NzJhOGMxYTg3ZTAtMCIsCiAgICAgICJhbmltYXRlZF9mYWNlIiA6ICJnZW9tZXRyeS5hbmltYXRlZF9mYWNlX3BlcnNvbmEtZTE5OTY3MmE4YzFhODdlMC0wIiwKICAgICAgImRlZmF1bHQiIDogImdlb21ldHJ5LnBlcnNvbmFfZTE5OTY3MmE4YzFhODdlMC0wIgogICB9Cn0K',
|
||||
SkinGeometryData: skinGeom,
|
||||
SkinImageHeight: 1,
|
||||
SkinImageWidth: 1,
|
||||
ArmSize: 'wide',
|
||||
CapeData: '',
|
||||
CapeId: '',
|
||||
CapeImageHeight: 0,
|
||||
CapeImageWidth: 0,
|
||||
CapeOnClassicSkin: false,
|
||||
PlatformOfflineId: '',
|
||||
PlatformOnlineId: '', // chat
|
||||
// a bunch of meaningless junk
|
||||
ClientRandomId: 1, // TODO make biggeer
|
||||
CurrentInputMode: 1,
|
||||
DefaultInputMode: 1,
|
||||
DeviceId: '2099de18-429a-465a-a49b-fc4710a17bb3', // TODO random
|
||||
DeviceModel: '',
|
||||
DeviceOS: client.session?.deviceOS || 7,
|
||||
GameVersion: options.version || '1.16.201',
|
||||
GuiScale: -1,
|
||||
UIProfile: 0,
|
||||
TenantId: '',
|
||||
PremiumSkin: false,
|
||||
PersonaSkin: false,
|
||||
LanguageCode: 'en_GB', // TODO locale
|
||||
PersonaPieces: [],
|
||||
PersonaSkin: true,
|
||||
PieceTintColors: [],
|
||||
PlatformOfflineId: '',
|
||||
PlatformOnlineId: '', // chat
|
||||
PremiumSkin: false,
|
||||
SelfSignedId: '78eb38a6-950e-3ab9-b2cf-dd849e343701',
|
||||
ServerAddress: `${options.hostname}:${options.port}`,
|
||||
SkinAnimationData: '',
|
||||
SkinColor: '#ffffcd96',
|
||||
SkinData: 'AAAAAA==',
|
||||
SkinGeometryData: skinGeom,
|
||||
SkinId: '5eb65f73-af11-448e-82aa-1b7b165316ad.persona-e199672a8c1a87e0-0',
|
||||
SkinImageHeight: 1,
|
||||
SkinImageWidth: 1,
|
||||
SkinResourcePatch: '',
|
||||
ThirdPartyName: client.profile.name,
|
||||
ThirdPartyNameOnly: false,
|
||||
SkinColor: '#ffffcd96'
|
||||
UIProfile: 0
|
||||
}
|
||||
payload = require('./logPack.json')
|
||||
const customPayload = options.userData || {}
|
||||
payload = { ...payload, ...customPayload }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
const { ClientStatus, Connection } = require('./connection')
|
||||
const { createDeserializer, createSerializer } = require('./transforms/serializer')
|
||||
const { RakClient } = require('./Rak')
|
||||
const { RakClient } = require('./rak')
|
||||
const { serialize } = require('./datatypes/util')
|
||||
const fs = require('fs')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
|
|
@ -14,6 +14,8 @@ const LoginVerify = require('./auth/loginVerify')
|
|||
const debugging = false
|
||||
|
||||
class Client extends Connection {
|
||||
connection
|
||||
|
||||
/** @param {{ version: number, hostname: string, port: number }} options */
|
||||
constructor (options) {
|
||||
super()
|
||||
|
|
@ -26,15 +28,19 @@ class Client extends Connection {
|
|||
Login(this, null, this.options)
|
||||
LoginVerify(this, null, this.options)
|
||||
|
||||
if (options.password) {
|
||||
auth.authenticatePassword(this, options)
|
||||
this.on('session', this.connect)
|
||||
|
||||
if (options.offline) {
|
||||
console.debug('offline mode, not authenticating', this.options)
|
||||
auth.createOfflineSession(this, this.options)
|
||||
} else if (options.password) {
|
||||
auth.authenticatePassword(this, this.options)
|
||||
} else {
|
||||
auth.authenticateDeviceCode(this, options)
|
||||
auth.authenticateDeviceCode(this, this.options)
|
||||
}
|
||||
|
||||
this.startGameData = {}
|
||||
|
||||
this.on('session', this.connect)
|
||||
this.startQueue()
|
||||
this.inLog = (...args) => debug('C ->', ...args)
|
||||
this.outLog = (...args) => debug('C <-', ...args)
|
||||
|
|
@ -64,14 +70,14 @@ class Client extends Connection {
|
|||
|
||||
this.connection = new RakClient({ useWorkers: true, hostname, port })
|
||||
this.connection.onConnected = () => this.sendLogin()
|
||||
this.connection.onCloseConnection = () => this._close()
|
||||
this.connection.onCloseConnection = () => this.close()
|
||||
this.connection.onEncapsulated = this.onEncapsulated
|
||||
this.connection.connect()
|
||||
}
|
||||
|
||||
sendLogin () {
|
||||
this.status = ClientStatus.Authenticating
|
||||
this.createClientChain()
|
||||
this.createClientChain(null, this.options.offline)
|
||||
|
||||
const chain = [
|
||||
this.clientIdentityChain, // JWT we generated for auth
|
||||
|
|
@ -79,7 +85,6 @@ class Client extends Connection {
|
|||
]
|
||||
|
||||
const encodedChain = JSON.stringify({ chain })
|
||||
|
||||
const bodyLength = this.clientUserChain.length + encodedChain.length + 8
|
||||
|
||||
debug('Auth chain', chain)
|
||||
|
|
@ -100,8 +105,8 @@ class Client extends Connection {
|
|||
process.exit(1) // TODO: handle
|
||||
}
|
||||
|
||||
onPlayStatus(statusPacket) {
|
||||
if (this.status == ClientStatus.Initializing && this.options.autoInitPlayer === true) {
|
||||
onPlayStatus (statusPacket) {
|
||||
if (this.status === ClientStatus.Initializing && this.options.autoInitPlayer === true) {
|
||||
if (statusPacket.status === 'player_spawn') {
|
||||
this.status = ClientStatus.Initialized
|
||||
this.write('set_local_player_as_initialized', { runtime_entity_id: this.startGameData.runtime_entity_id })
|
||||
|
|
@ -110,14 +115,12 @@ class Client extends Connection {
|
|||
}
|
||||
}
|
||||
|
||||
_close() {
|
||||
close () {
|
||||
clearInterval(this.loop)
|
||||
this.q = []
|
||||
this.q2 = []
|
||||
}
|
||||
|
||||
close () {
|
||||
this._close()
|
||||
this.connection.close()
|
||||
this.connection?.close()
|
||||
this.removeAllListeners()
|
||||
console.log('Closed!')
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const { MsAuthFlow } = require('./authFlow.js')
|
||||
const { uuidFrom } = require('../datatypes/util')
|
||||
|
||||
/**
|
||||
* Obtains Minecaft profile data using a Minecraft access token and starts the join sequence
|
||||
|
|
@ -27,6 +28,22 @@ async function postAuthenticate (client, options, chains) {
|
|||
client.emit('session', profile)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an offline session for the client
|
||||
*/
|
||||
function createOfflineSession (client, options) {
|
||||
if (!options.username) throw Error('Must specify a valid username')
|
||||
const profile = {
|
||||
name: options.username,
|
||||
uuid: uuidFrom(options.username), // random
|
||||
xuid: 0
|
||||
}
|
||||
client.profile = profile
|
||||
client.username = profile.name
|
||||
client.accessToken = [] // No extra JWTs, only send 1 client signed chain with all the data
|
||||
client.emit('session', profile)
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticates with Mincrosoft through user credentials, then
|
||||
* with Xbox Live, Minecraft, checks entitlements and returns profile
|
||||
|
|
@ -61,6 +78,7 @@ async function authenticateDeviceCode (client, options) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
createOfflineSession,
|
||||
authenticatePassword,
|
||||
authenticateDeviceCode
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const fs = require('fs')
|
||||
const UUID = require('uuid-1345')
|
||||
|
||||
function getFiles (dir) {
|
||||
let results = []
|
||||
|
|
@ -19,15 +20,23 @@ function sleep (ms) {
|
|||
return new Promise(resolve => setTimeout(resolve, ms))
|
||||
}
|
||||
|
||||
function waitFor (cb, withTimeout) {
|
||||
return Promise.race([
|
||||
new Promise((resolve) => cb(resolve)),
|
||||
sleep(withTimeout)
|
||||
async function waitFor (cb, withTimeout, onTimeout) {
|
||||
let t
|
||||
const ret = await Promise.race([
|
||||
new Promise(resolve => cb(resolve)),
|
||||
new Promise(resolve => { t = setTimeout(() => resolve('timeout'), withTimeout) })
|
||||
])
|
||||
clearTimeout(t)
|
||||
if (ret === 'timeout') onTimeout()
|
||||
return ret
|
||||
}
|
||||
|
||||
function serialize (obj = {}, fmt) {
|
||||
return JSON.stringify(obj, (k, v) => typeof v === 'bigint' ? v.toString() : v, fmt)
|
||||
}
|
||||
|
||||
module.exports = { getFiles, sleep, waitFor, serialize }
|
||||
function uuidFrom (string) {
|
||||
return UUID.v3({ namespace: '6ba7b811-9dad-11d1-80b4-00c04fd430c8', name: string })
|
||||
}
|
||||
|
||||
module.exports = { getFiles, sleep, waitFor, serialize, uuidFrom }
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ const defaultOptions = {
|
|||
// https://minecraft.gamepedia.com/Protocol_version#Bedrock_Edition_2
|
||||
version: CURRENT_VERSION,
|
||||
// client: If we should send SetPlayerInitialized to the server after getting play_status spawn.
|
||||
// if this is disabled, no 'spawn' event will be emitted, you should manually set
|
||||
// if this is disabled, no 'spawn' event will be emitted, you should manually set
|
||||
// client.status to ClientStatus.Initialized after sending the init packet.
|
||||
autoInitPlayer: true
|
||||
autoInitPlayer: true,
|
||||
// If true, do not authenticate with Xbox Live
|
||||
offline: false
|
||||
}
|
||||
|
||||
const Versions = {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ class RakNativeClient extends EventEmitter {
|
|||
this.raknet.connect()
|
||||
}
|
||||
|
||||
close() {
|
||||
close () {
|
||||
this.connected = false
|
||||
setTimeout(() => {
|
||||
this.raknet.close()
|
||||
|
|
@ -99,7 +99,7 @@ class RakNativeServer extends EventEmitter {
|
|||
this.raknet.listen()
|
||||
}
|
||||
|
||||
close() {
|
||||
close () {
|
||||
this.raknet.close()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class Player extends Connection {
|
|||
*/
|
||||
sendDisconnectStatus (playStatus) {
|
||||
this.write('play_status', { status: playStatus })
|
||||
this.connection.close()
|
||||
this.close()
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -82,7 +82,7 @@ class Player extends Connection {
|
|||
hide_disconnect_screen: hide,
|
||||
message: reason
|
||||
})
|
||||
this.connection.close()
|
||||
this.close()
|
||||
}
|
||||
|
||||
// After sending Server to Client Handshake, this handles the client's
|
||||
|
|
@ -95,6 +95,14 @@ class Player extends Connection {
|
|||
this.emit('join')
|
||||
}
|
||||
|
||||
close () {
|
||||
this.q = []
|
||||
this.q2 = []
|
||||
clearInterval(this.loop)
|
||||
this.connection?.close()
|
||||
this.removeAllListeners()
|
||||
}
|
||||
|
||||
readPacket (packet) {
|
||||
// console.log('packet', packet)
|
||||
try {
|
||||
|
|
|
|||
62
test/vanilla.js
Normal file
62
test/vanilla.js
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// process.env.DEBUG = 'minecraft-protocol raknet'
|
||||
const vanillaServer = require('../tools/startVanillaServer')
|
||||
const { Client } = require('../src/client')
|
||||
const { waitFor } = require('../src/datatypes/util')
|
||||
|
||||
async function test () {
|
||||
// Start the server, wait for it to accept clients, throws on timeout
|
||||
const handle = await vanillaServer.startServerAndWait('1.16.201', 1000 * 120)
|
||||
console.log('Started server')
|
||||
|
||||
const client = new Client({
|
||||
hostname: '127.0.0.1',
|
||||
port: 19130,
|
||||
username: 'Notch',
|
||||
offline: true
|
||||
})
|
||||
|
||||
console.log('Started client')
|
||||
|
||||
let loop
|
||||
|
||||
await waitFor((res) => {
|
||||
client.once('resource_packs_info', (packet) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
|
||||
client.once('resource_pack_stack', (stack) => {
|
||||
client.write('resource_pack_client_response', {
|
||||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
})
|
||||
|
||||
client.queue('client_cache_status', { enabled: false })
|
||||
client.queue('request_chunk_radius', { chunk_radius: 1 })
|
||||
|
||||
clearInterval(loop)
|
||||
loop = setInterval(() => {
|
||||
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: BigInt(Date.now()) })
|
||||
}, 200)
|
||||
|
||||
console.log('Awaiting join')
|
||||
|
||||
client.on('spawn', () => {
|
||||
console.log('✔ Client has spawned')
|
||||
client.close()
|
||||
handle.kill()
|
||||
res()
|
||||
})
|
||||
})
|
||||
}, 1000 * 60, () => {
|
||||
client.close()
|
||||
handle.kill()
|
||||
throw Error('❌ client timed out ')
|
||||
})
|
||||
clearInterval(loop)
|
||||
}
|
||||
|
||||
if (!module.parent) test()
|
||||
module.exports = { clientTest: test }
|
||||
10
test/vanilla.test.js
Normal file
10
test/vanilla.test.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/* eslint-env jest */
|
||||
|
||||
const { clientTest } = require('./vanilla')
|
||||
|
||||
describe('vanilla server test', function () {
|
||||
this.timeout(120 * 1000)
|
||||
it('client spawns', async () => {
|
||||
await clientTest()
|
||||
})
|
||||
})
|
||||
102
tools/startVanillaServer.js
Normal file
102
tools/startVanillaServer.js
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
const http = require('https')
|
||||
const fs = require('fs')
|
||||
const cp = require('child_process')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
const { getFiles, waitFor } = require('../src/datatypes/util')
|
||||
|
||||
const head = (url) => new Promise((resolve, reject) => http.request(url, { method: 'HEAD' }, resolve).on('error', reject).end())
|
||||
const get = (url, out) => cp.execSync(`curl -o ${out} ${url}`)
|
||||
|
||||
// Get the latest versions
|
||||
// TODO: once we support multi-versions
|
||||
function fetchLatestStable () {
|
||||
get('https://raw.githubusercontent.com/minecraft-linux/mcpelauncher-versiondb/master/versions.json', 'versions.json')
|
||||
const versions = JSON.parse(fs.readFileSync('./versions.json'))
|
||||
const latest = versions[versions.length - 1]
|
||||
return latest.version_name
|
||||
}
|
||||
|
||||
// Download + extract vanilla server and enter the directory
|
||||
async function download (os, version) {
|
||||
process.chdir(__dirname)
|
||||
const verStr = version.split('.').slice(0, 3).join('.')
|
||||
const dir = 'bds-' + version
|
||||
|
||||
if (fs.existsSync(dir) && getFiles(dir).length) {
|
||||
process.chdir('bds-' + version) // Enter server folder
|
||||
return verStr
|
||||
}
|
||||
try { fs.mkdirSync(dir) } catch { }
|
||||
|
||||
process.chdir('bds-' + version) // Enter server folder
|
||||
const url = (os, version) => `https://minecraft.azureedge.net/bin-${os}/bedrock-server-${version}.zip`
|
||||
|
||||
let found = false
|
||||
|
||||
for (let i = 0; i < 8; 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)
|
||||
const ret = await head(u)
|
||||
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)
|
||||
get(found, 'bds.zip')
|
||||
console.info('⚡ Unzipping')
|
||||
// Unzip server
|
||||
if (process.platform === 'linux') cp.execSync('unzip bds.zip')
|
||||
else cp.execSync('tar -xf bds.zip')
|
||||
return verStr
|
||||
}
|
||||
|
||||
// Setup the server
|
||||
function configure () {
|
||||
let config = fs.readFileSync('./server.properties', 'utf-8')
|
||||
config += '\nlevel-generator=2\nserver-port=19130\nplayer-idle-timeout=1\nallow-cheats=true\ndefault-player-permission-level=operator\nonline-mode=false'
|
||||
fs.writeFileSync('./server.properties', config)
|
||||
}
|
||||
|
||||
function run (inheritStdout = true) {
|
||||
const exe = process.platform === 'win32' ? 'bedrock_server.exe' : './bedrock_server'
|
||||
return cp.spawn(exe, inheritStdout ? { stdio: 'inherit' } : {})
|
||||
}
|
||||
|
||||
// Run the server
|
||||
async function startServer (version, onStart) {
|
||||
const os = process.platform === 'win32' ? 'win' : process.platform
|
||||
if (os !== 'win' && os !== 'linux') {
|
||||
throw Error('unsupported os ' + os)
|
||||
}
|
||||
await download(os, version)
|
||||
configure()
|
||||
const handle = run(!onStart)
|
||||
if (onStart) {
|
||||
handle.stdout.on('data', data => data.includes('Server started.') ? onStart() : null)
|
||||
handle.stdout.pipe(process.stdout)
|
||||
handle.stderr.pipe(process.stdout)
|
||||
}
|
||||
return handle
|
||||
}
|
||||
|
||||
// Start the server and wait for it to be ready, with a timeout
|
||||
async function startServerAndWait (version, withTimeout) {
|
||||
let handle
|
||||
await waitFor(async res => {
|
||||
handle = await startServer(version, res)
|
||||
}, withTimeout, () => {
|
||||
handle?.kill()
|
||||
throw new Error('Server did not start on time ' + withTimeout)
|
||||
})
|
||||
return handle
|
||||
}
|
||||
|
||||
if (!module.parent) {
|
||||
// if (process.argv.length < 3) throw Error('Missing version argument')
|
||||
startServer(process.argv[2] || '1.16.201')
|
||||
}
|
||||
|
||||
module.exports = { fetchLatestStable, startServer, startServerAndWait }
|
||||
Loading…
Add table
Add a link
Reference in a new issue