smac
Version:
Scriptcraft SMA Server controller
366 lines (334 loc) • 11.7 kB
text/typescript
import chalk from 'chalk'
import * as fs from 'fs-extra'
import { Maybe, Nothing, Result } from 'ghetto-monad'
import * as path from 'path'
import * as docker from './docker'
import { Binder } from './docker'
import { localPath, localWorldsPath } from './paths'
import { RestConfig, SMAServerConfig } from './SMAServerConfig'
import { World } from './worlds'
export type ServerType = 'bukkit' | 'nukkit'
class Server {
static defaultServerType: ServerType = 'bukkit'
static defaultPort = {
bukkit: '25565',
nukkit: '19132',
}
static defaultDockerTag = 'latest'
static defaultMemory = 2048
static restPort = 8086
static restPassword = 'INSECURE'
static defaultDockerImage = 'magikcraft/scriptcraft'
private serverConfig!: Promise<Maybe<Result<SMAServerConfig>>>
binder: docker.Binder
filename!: string
serverType!: ServerType
constructor() {
this.binder = new Binder()
}
async getServerTargetFromPackageJson() {
const conf = await this.getServerConfig()
if (conf.isNothing) {
return new Nothing()
} else {
return new Result(conf.value.serverName)
}
}
async getNodeModulesBinding() {
const conf = await this.getServerConfig()
if (conf.isNothing) {
return new Nothing()
} else {
console.log(`Bind node_modules: ${conf.value.node_modules}`)
return new Result(conf.value.node_modules)
}
}
async getTestMode() {
const conf = await this.getServerConfig()
if (conf.isNothing) {
return false
} else {
const testMode = conf.value.testMode === true
return testMode
}
}
async getName() {
const conf = await this.getServerConfig()
if (conf.isNothing) {
return undefined
} else {
return conf.value.serverName
}
}
// @TODO Bind node_modules directly
createNodeModuleBindings() {
const modules = fs.readdirSync('node_modules')
if (!modules) {
console.log(
chalk.yellow(
'WARNING: node_modules directory not found, and it was specified in the bindings. Do you need to run '
) +
chalk.magenta('npm i') +
chalk.yellow('?')
)
console.log('Skipping node_modules binding')
return ''
}
return this.binder.makeMount(
localPath(`node_modules`),
`scriptcraft-plugins/node_modules`
)
// const nsPackage = m => {
// const isNamespacedPackage = m && m.indexOf('@') === 0
// if (isNamespacedPackage) {
// const pkgs = fs.readdirSync(`node_modules/${m}`)
// return pkgs
// .map(p =>
// this.binder.makeMount(
// localPath(`node_modules/${m}/${p}`),
// `scriptcraft-plugins/${m}/${p}`
// )
// )
// .join(' ')
// }
// return this.binder.makeMount(
// localPath(`node_modules/${m}`),
// `scriptcraft-plugins/${m}`
// )
// }
// if (modules.length > 0) {
// return modules.map(nsPackage).join(' ')
// }
// return ''
}
async getBindings(name) {
const worlds = await this.getWorldMounts()
const bindings = (await this.getCustomBindings())
.map(({ src, dst }) => this.binder.makeMount(localPath(src), dst))
.join(' ')
const mountNodeModules = await this.getNodeModulesBinding()
const nodeModules =
!mountNodeModules.isNothing && mountNodeModules.value
? this.createNodeModuleBindings()
: ``
console.log('Found bindings in config:')
console.log(bindings)
return `${worlds} ${bindings} ${nodeModules}`
}
private async getWorldMounts() {
// Check for worlds in the local worlds folder
const localMounts = this.getLocalWorldMounts()
// Parse worldDefinitions and make mounts
const smaMounts = await this.getSmaWorldMounts()
// Make them unique - prefer local
const allMounts = {} as {
[index: string]: { src: string; dst: string }
}
localMounts.map(({ src, dst }) => {
allMounts[dst] = { src, dst }
})
for (const smaMount of smaMounts) {
// Do we need to scan these dirs?
console.log(`Found: ${smaMount.src}`)
const existingMount = allMounts[smaMount.dst]
if (existingMount) {
if (smaMount.src !== existingMount.src) {
console.log(
chalk.redBright(
`Duplicate worlds found at ${smaMount.src} and ${
existingMount.src
}`
)
)
console.log(
chalk.yellowBright(
`Using world from ${existingMount.src}`
)
)
}
} else {
allMounts[smaMount.dst] = smaMount
}
}
if (Object.keys(allMounts).length > 0) {
console.log(`Loading the following worlds:`)
}
return Object.keys(allMounts)
.map(m => {
console.log(allMounts[m])
const r = this.binder.makeMount(
allMounts[m].src,
allMounts[m].dst
)
return r
})
.join(' ')
}
private getLocalWorldMounts() {
const mountData = (name, path) => ({
src: `${path}/${name}`,
dst: `worlds/${name}`,
})
const localPath = localWorldsPath()
console.log('Scanning local directory:', localPath)
if (fs.existsSync(localPath)) {
const dirs = fs.readdirSync(localPath)
return dirs.map(name => {
console.log('Found:', path.join(localPath, name))
const m = mountData(name, localPath)
return m
})
}
return []
}
private async getSmaWorldMounts() {
const mountData = (name, path) => ({
src: `${path}/${name}`,
dst: `worlds/${name}`,
})
console.log(`Checking world definitions in ${this.filename}`)
const worldDefs = await this.getWorldDefinitions()
if (worldDefs.isNothing) {
console.log('None found.')
return []
}
const worlds = worldDefs.value.map(d => new World(d))
let smaMounts = [] as { src: string; dst: string }[]
for (const world of worlds) {
const path = await world.getPath()
if (!path.isNothing && !path.isError) {
if (fs.existsSync(path.value)) {
const dirs = fs.readdirSync(path.value)
dirs.map(name => {
smaMounts = [...smaMounts, mountData(name, path.value)]
})
return smaMounts
}
}
}
if (smaMounts.length != worlds.length) {
console.log(
chalk.red(
'WARNING: Some worlds specified in the Worlds definition are not available.'
)
)
}
return smaMounts
}
async getDockerTag() {
const conf = await this.getServerConfig()
if (conf.isNothing || !conf.value.dockerTag) {
return Server.defaultDockerTag
} else {
return conf.value.dockerTag
}
}
async getServerType() {
const conf = await this.getServerConfig()
if (conf.isNothing || !conf.value.serverType) {
return Server.defaultServerType
} else {
return conf.value.serverType
}
}
async getDockerImage() {
const serverType = await this.getServerType()
if (serverType === 'bukkit') {
return docker.images.bukkit
}
if (serverType === 'nukkit') {
return docker.images.nukkit
}
return docker.images.bukkit
}
async getContainerPort() {
const serverType = await this.getServerType()
return Server.defaultPort[serverType]
}
async getExposedPort() {
const conf = await this.getServerConfig()
if (conf.isNothing || !conf.value.port) {
return this.getContainerPort()
} else {
return conf.value.port
}
}
async getMemoryConfig() {
const conf = await this.getServerConfig()
if (conf.isNothing || !conf.value.memory) {
return Server.defaultMemory
} else {
return conf.value.memory
}
}
async getRestConfig(): Promise<RestConfig> {
const conf = await this.getServerConfig()
const defaultConfig = {
port: Server.restPort,
password: Server.restPassword,
}
if (conf.isNothing || !conf.value.restEndpoint) {
return defaultConfig
} else {
return { ...defaultConfig, ...conf.value.restEndpoint }
}
}
async getEnvironment() {
const memory = await this.getMemoryConfig()
const restConfig = await this.getRestConfig()
const env = [] as any
env.push(`-e SERVERMEM=${memory}`)
env.push(`-e MINECRAFT_REST_CONSOLE_PORT=${restConfig.port}`)
env.push(`-e MINECRAFT_REST_CONSOLE_API_KEY=${restConfig.password}`)
return env.join(' ')
}
private async getCustomBindings() {
const conf = await this.getServerConfig()
if (conf.isNothing || !conf.value.bind) {
return []
} else {
return conf.value.bind
}
}
private checkForConfigFile(filename: string) {
const cwd = process.cwd()
const pkgPath = path.join(cwd, filename)
if (!fs.existsSync(pkgPath)) {
return undefined
} else {
return pkgPath
}
}
private async getServerConfig() {
if (this.serverConfig) {
return this.serverConfig
}
const pkgPath = this.filename
? this.checkForConfigFile(this.filename)
: this.checkForConfigFile('smac.json') ||
this.checkForConfigFile('package.json')
if (!pkgPath) {
this.serverConfig = Promise.resolve(new Nothing())
return new Nothing()
}
const md = await import(pkgPath)
if (!md.smaServerConfig) {
this.serverConfig = Promise.resolve(new Nothing())
return new Nothing()
}
console.log(`Loading settings from ${pkgPath}`)
this.serverConfig = Promise.resolve(
new Result<SMAServerConfig>(md.smaServerConfig)
)
return new Result<SMAServerConfig>(md.smaServerConfig)
}
private async getWorldDefinitions() {
const conf = await this.getServerConfig()
if (conf.isNothing || !conf.value.worlds) {
return new Nothing()
} else {
return new Result(conf.value.worlds)
}
}
}
export const server = new Server()