Backend cleanup (#146)

* Add ServerRegistration, begin refactoring to match frontend

* move graphData logic into ServerRegistration

* move ping updates/history into ServerRegistration

* start updating main app entry methods

* fix default rates.updateMojangStatus

* fix record loading delays on freshly booted instances

* move database loading logic to method + callback

* use data in frontend for type lookup instead of ping

* cleanup app.js

* reorganize methods to improve flow

* avoid useless mojang updates, remove legacy fields

* rename legacy fields for consistency

* finish restructure around App model

* ensure versions are sorted by release order

* filter errors sent to frontend to avoid data leaks

* fix version listing behavior on frontend

* 5.1.0
This commit is contained in:
Nick Krecklow 2020-04-21 17:59:53 -05:00 committed by GitHub
parent 9eda8d6bdb
commit 4d13965e6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 822 additions and 823 deletions

392
app.js

@ -1,392 +0,0 @@
/**
* THIS IS LEGACY, UNMAINTAINED CODE
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
* USAGE IS NOT RECOMMENDED
*/
var server = require('./lib/server');
var ping = require('./lib/ping');
var logger = require('./lib/logger');
var mojang = require('./lib/mojang_services');
var util = require('./lib/util');
var db = require('./lib/database');
var config = require('./config.json');
var servers = require('./servers.json');
var minecraftVersions = require('./minecraft_versions.json');
var networkHistory = [];
var connectedClients = 0;
var networkVersions = [];
const lastFavicons = [];
var graphData = [];
var highestPlayerCount = {};
var lastGraphPush = [];
var graphPeaks = {};
const serverProtocolVersionIndexes = []
function getNextProtocolVersion (server) {
// Minecraft Bedrock Edition does not have protocol versions
if (server.type === 'PE') {
return {
protocolId: 0,
protocolIndex: 0
}
}
const protocolVersions = minecraftVersions[server.type]
let nextProtocolVersion = serverProtocolVersionIndexes[server.name]
if (typeof nextProtocolVersion === 'undefined' || nextProtocolVersion + 1 >= protocolVersions.length) {
nextProtocolVersion = 0
} else {
nextProtocolVersion++
}
serverProtocolVersionIndexes[server.name] = nextProtocolVersion
return {
protocolId: protocolVersions[nextProtocolVersion].protocolId,
protocolIndex: nextProtocolVersion
}
}
function pingAll() {
for (var i = 0; i < servers.length; i++) {
// Make sure we lock our scope.
(function(network) {
// Asign auto generated color if not present
if (!network.color) {
network.color = util.stringToColor(network.name);
}
const attemptedVersion = getNextProtocolVersion(network)
ping.ping(network.ip, network.port, network.type, config.rates.connectTimeout, function(err, res) {
// Handle our ping results, if it succeeded.
if (err) {
logger.log('error', 'Failed to ping ' + network.ip + ': ' + err.message);
}
// If we have favicon override specified, use it.
if (network.favicon) {
res.favicon = network.favicon
}
handlePing(network, res, err, attemptedVersion);
}, attemptedVersion.protocolId);
})(servers[i]);
}
}
// This is where the result of a ping is feed.
// This stores it and converts it to ship to the frontend.
function handlePing(network, res, err, attemptedVersion) {
// Log our response.
if (!networkHistory[network.name]) {
networkHistory[network.name] = [];
}
// Update the version list
if (!networkVersions[network.name]) {
networkVersions[network.name] = [];
}
const serverVersionHistory = networkVersions[network.name]
// If the result version matches the attempted version, the version is supported
if (res && res.version !== undefined) {
const indexOf = serverVersionHistory.indexOf(attemptedVersion.protocolIndex)
// Test indexOf to avoid inserting previously recorded protocolIndex values
if (res.version === attemptedVersion.protocolId && indexOf === -1) {
serverVersionHistory.push(attemptedVersion.protocolIndex)
} else if (res.version !== attemptedVersion.protocolId && indexOf >= 0) {
serverVersionHistory.splice(indexOf, 1)
}
}
const timestamp = util.getCurrentTimeMs()
if (res) {
const recordData = highestPlayerCount[network.ip]
// Validate that we have logToDatabase enabled otherwise in memory pings
// will create a record that's only valid for the runtime duration.
if (config.logToDatabase && (!recordData || res.players.online > recordData.playerCount)) {
highestPlayerCount[network.ip] = {
playerCount: res.players.online,
timestamp: timestamp
}
}
}
// Update the clients
var networkSnapshot = {
info: {
name: network.name,
timestamp: timestamp,
type: network.type
},
versions: serverVersionHistory,
recordData: highestPlayerCount[network.ip]
};
if (res) {
networkSnapshot.result = res;
// Only emit updated favicons
// Favicons will otherwise be explicitly emitted during the handshake process
if (res.favicon) {
const lastFavicon = lastFavicons[network.name]
if (lastFavicon !== res.favicon) {
lastFavicons[network.name] = res.favicon
networkSnapshot.favicon = res.favicon // Send updated favicon directly on object
}
delete res.favicon // Never store favicons in memory outside lastFavicons
}
} else if (err) {
networkSnapshot.error = err;
}
server.io.sockets.emit('update', networkSnapshot);
var _networkHistory = networkHistory[network.name];
// Remove our previous data that we don't need anymore.
for (var i = 0; i < _networkHistory.length; i++) {
delete _networkHistory[i].versions
delete _networkHistory[i].info;
}
_networkHistory.push({
error: err,
result: res,
versions: serverVersionHistory,
timestamp: timestamp,
info: {
ip: network.ip,
port: network.port,
type: network.type,
name: network.name
}
});
// Make sure we never log too much.
if (_networkHistory.length > 72) { // 60/2.5 = 24, so 24 is one minute
_networkHistory.shift();
}
// Log it to the database if needed.
if (config.logToDatabase) {
db.log(network.ip, timestamp, res ? res.players.online : 0);
}
// The same mechanic from trimUselessPings is seen here.
// If we dropped the ping, then to avoid destroying the graph, ignore it.
// However if it's been too long since the last successful ping, we'll send it anyways.
if (config.logToDatabase) {
if (!lastGraphPush[network.ip] || (timestamp - lastGraphPush[network.ip] >= 60 * 1000 && res) || timestamp - lastGraphPush[network.ip] >= 70 * 1000) {
lastGraphPush[network.ip] = timestamp;
// Don't have too much data!
util.trimOldPings(graphData);
if (!graphData[network.name]) {
graphData[network.name] = [];
}
graphData[network.name].push([timestamp, res ? res.players.online : 0]);
// Send the update.
server.io.sockets.emit('updateHistoryGraph', {
ip: network.ip,
name: network.name,
players: (res ? res.players.online : 0),
timestamp: timestamp
});
}
// Update calculated graph peak regardless if the graph is being updated
// This can cause a (harmless) desync between live and stored data, but it allows it to be more accurate for long surviving processes
var networkData = graphData[network.name];
if (networkData) {
var graphPeakIndex = -1;
var graphPeakPlayerCount = 0;
for (var i = 0; i < networkData.length; i++) {
// [1] refers to the online player count
var point = networkData[i][1];
if (point > 0 && (graphPeakIndex === -1 || point > graphPeakPlayerCount)) {
graphPeakIndex = i;
graphPeakPlayerCount = point;
}
}
// Test if a highest index has been selected and has changed from any previous selections
var previousPeak = graphPeaks[network.name];
// [1] refers to the online player count
if (graphPeakIndex !== -1 && (!previousPeak || previousPeak[1] !== graphPeakPlayerCount)) {
var graphPeakData = networkData[graphPeakIndex];
graphPeaks[network.name] = graphPeakData;
// Broadcast update event to clients
server.io.sockets.emit('updatePeak', {
ip: network.ip,
name: network.name,
players: graphPeakData[1],
timestamp: graphPeakData[0]
});
}
}
}
}
// Start our main loop that does everything.
function startMainLoop() {
util.setIntervalNoDelay(pingAll, config.rates.pingAll);
util.setIntervalNoDelay(function() {
mojang.update(config.rates.mojangStatusTimeout);
server.io.sockets.emit('updateMojangServices', mojang.toMessage());
}, config.rates.upateMojangStatus);
}
function startServices() {
server.start();
// Track how many people are currently connected.
server.io.on('connect', function(client) {
// We're good to connect them!
connectedClients += 1;
logger.log('info', '%s connected, total clients: %d', util.getRemoteAddr(client.request), connectedClients);
// Attach our listeners.
client.on('disconnect', function() {
connectedClients -= 1;
logger.log('info', '%s disconnected, total clients: %d', util.getRemoteAddr(client.request), connectedClients);
});
client.on('requestHistoryGraph', function() {
if (config.logToDatabase) {
// Send them the big 24h graph.
client.emit('historyGraph', graphData);
// Send current peaks, if any
if (Object.keys(graphPeaks).length > 0) {
client.emit('peaks', graphPeaks);
}
}
});
const minecraftVersionNames = {}
Object.keys(minecraftVersions).forEach(function (key) {
minecraftVersionNames[key] = minecraftVersions[key].map(version => version.name)
})
// Send configuration data for rendering the page
client.emit('setPublicConfig', {
graphDuration: config.graphDuration,
servers: servers,
minecraftVersions: minecraftVersionNames,
isGraphVisible: config.logToDatabase
});
// Send them our previous data, so they have somewhere to start.
client.emit('updateMojangServices', mojang.toMessage());
// Send each individually, this should look cleaner than waiting for one big array to transfer.
for (var i = 0; i < servers.length; i++) {
var server = servers[i];
if (!(server.name in networkHistory) || networkHistory[server.name].length < 1) {
// This server hasn't been ping'd yet. Send a hacky placeholder.
client.emit('add', [[{
error: {
description: 'Waiting...',
placeholder: true
},
result: null,
timestamp: util.getCurrentTimeMs(),
info: {
ip: server.ip,
port: server.port,
type: server.type,
name: server.name
}
}]]);
} else {
// Append the lastFavicon to the last ping entry
const serverHistory = networkHistory[server.name];
const lastFavicon = lastFavicons[server.name];
if (lastFavicon) {
serverHistory[serverHistory.length - 1].favicon = lastFavicon
}
client.emit('add', [serverHistory])
}
}
client.emit('syncComplete');
});
startMainLoop();
}
logger.log('info', 'Booting, please wait...');
if (config.logToDatabase) {
// Setup our database.
db.setup();
var timestamp = util.getCurrentTimeMs();
db.queryPings(config.graphDuration, function(data) {
graphData = util.convertServerHistory(data);
completedQueries = 0;
logger.log('info', 'Queried and parsed ping history in %sms', util.getCurrentTimeMs() - timestamp);
for (var i = 0; i < servers.length; i++) {
// Compute graph peak from historical data
var networkData = graphData[servers[i].name];
if (networkData) {
var graphPeakIndex = -1;
var graphPeakPlayerCount = 0;
for (var x = 0; x < networkData.length; x++) {
// [1] refers to the online player count
var point = networkData[x][1];
if (point > 0 && (graphPeakIndex === -1 || point > graphPeakPlayerCount)) {
graphPeakIndex = x;
graphPeakPlayerCount = point;
}
}
if (graphPeakIndex !== -1) {
graphPeaks[servers[i].name] = networkData[graphPeakIndex];
logger.log('info', 'Selected graph peak %d (%s)', networkData[graphPeakIndex][1], servers[i].name);
}
}
(function(server) {
db.getTotalRecord(server.ip, function(playerCount, timestamp) {
logger.log('info', 'Computed total record %s (%d) @ %d', server.ip, playerCount, timestamp);
highestPlayerCount[server.ip] = {
playerCount: playerCount,
timestamp: timestamp
};
completedQueries += 1;
if (completedQueries === servers.length) {
startServices();
}
});
})(servers[i]);
}
});
} else {
logger.log('warn', 'Database logging is not enabled. You can enable it by setting "logToDatabase" to true in config.json. This requires sqlite3 to be installed.');
startServices();
}

@ -69,7 +69,7 @@ document.addEventListener('DOMContentLoaded', function () {
const serverRegistration = app.serverRegistry.getServerRegistration(data.name)
if (serverRegistration) {
app.graphDisplayManager.addGraphPoint(serverRegistration.serverId, data.timestamp, data.players)
app.graphDisplayManager.addGraphPoint(serverRegistration.serverId, data.timestamp, data.playerCount)
// Only redraw the graph if not mutating hidden data
if (serverRegistration.isVisible) {
@ -94,7 +94,7 @@ document.addEventListener('DOMContentLoaded', function () {
})
socket.on('updateMojangServices', function (data) {
Object.values(data).forEach(app.mojangUpdater.updateServiceStatus)
app.mojangUpdater.updateStatus(data)
})
socket.on('setPublicConfig', function (data) {
@ -125,7 +125,7 @@ document.addEventListener('DOMContentLoaded', function () {
const serverRegistration = app.serverRegistry.getServerRegistration(data.name)
if (serverRegistration) {
serverRegistration.updateServerPeak(data.timestamp, data.players)
serverRegistration.updateServerPeak(data.timestamp, data.playerCount)
}
})
@ -134,10 +134,9 @@ document.addEventListener('DOMContentLoaded', function () {
const serverRegistration = app.serverRegistry.getServerRegistration(serverName)
if (serverRegistration) {
const graphData = data[serverName]
const graphPeak = data[serverName]
// [0] and [1] indexes correspond to flot.js' graphing data structure
serverRegistration.updateServerPeak(graphData[0], graphData[1])
serverRegistration.updateServerPeak(graphPeak.timestamp, graphPeak.playerCount)
}
})
})

@ -1,10 +1,16 @@
const MOJANG_STATUS_BASE_CLASS = 'header-button header-button-group'
export class MojangUpdater {
updateServiceStatus (status) {
updateStatus (services) {
for (const name of Object.keys(services)) {
this.updateServiceStatus(name, services[name])
}
}
updateServiceStatus (name, title) {
// HACK: ensure mojang-status is added for alignment, replace existing class to swap status color
document.getElementById('mojang-status_' + status.name).setAttribute('class', MOJANG_STATUS_BASE_CLASS + ' mojang-status-' + status.title.toLowerCase())
document.getElementById('mojang-status-text_' + status.name).innerText = status.title
document.getElementById('mojang-status_' + name).setAttribute('class', MOJANG_STATUS_BASE_CLASS + ' mojang-status-' + title.toLowerCase())
document.getElementById('mojang-status-text_' + name).innerText = title
}
reset () {

@ -204,7 +204,7 @@ export class ServerRegistration {
const versionsElement = document.getElementById('version_' + this.serverId)
versionsElement.style.display = 'block'
versionsElement.innerText = formatMinecraftVersions(ping.versions, minecraftVersions[ping.info.type]) || ''
versionsElement.innerText = formatMinecraftVersions(ping.versions, minecraftVersions[this.data.type]) || ''
}
// Compare against a cached value to avoid empty updates
@ -237,8 +237,7 @@ export class ServerRegistration {
playerCountLabelElement.style.display = 'none'
errorElement.style.display = 'block'
// Attempt to find an error cause from documented options
errorElement.innerText = ping.error.description || ping.error.errno || 'Unknown error'
errorElement.innerText = ping.error.message
} else if (ping.result) {
// Ensure the player-count element is visible and hide the error element
playerCountLabelElement.style.display = 'block'

@ -64,16 +64,14 @@ export function formatMinecraftVersions (versions, knownVersions) {
const versionGroups = []
for (let i = 0; i < versions.length; i++) {
const versionIndex = versions[i]
// Look for value mismatch between the previous index
// Require i > 0 since lastVersionIndex is undefined for i === 0
if (i > 0 && versions[i] - 1 !== versionIndex - 1) {
if (i > 0 && versions[i] - versions[i - 1] !== 1) {
versionGroups.push(currentVersionGroup)
currentVersionGroup = []
}
currentVersionGroup.push(versionIndex)
currentVersionGroup.push(versions[i])
}
// Ensure the last versionGroup is always pushed

@ -4,7 +4,7 @@
"ip": "0.0.0.0"
},
"rates": {
"upateMojangStatus": 5000,
"updateMojangStatus": 5000,
"mojangStatusTimeout": 3500,
"pingAll": 3000,
"connectTimeout": 2500

@ -1,4 +1,7 @@
**5** *(Apr 8 2020)*
**5.1.0** *(Apr 21 2020)*
- Completely rebuilt the backend. This includes several optimizations, code cleanup and syncing fixes. Its code model now pairs nicely with the frontend's Javascript model.
**5.0.0** *(Apr 8 2020)*
- New logo!
- Completely rebuilt the frontend's Javascript (heavy optimizations and cleanup!)
- Adds a button for mobile devices to manually request the historical graph

92
lib/app.js Normal file

@ -0,0 +1,92 @@
const Database = require('./database')
const MojangUpdater = require('./mojang')
const PingController = require('./ping')
const Server = require('./server')
const config = require('../config')
const minecraftVersions = require('../minecraft_versions')
class App {
serverRegistrations = []
constructor () {
this.mojangUpdater = new MojangUpdater(this)
this.pingController = new PingController(this)
this.server = new Server(this.handleClientConnection)
}
loadDatabase (callback) {
this.database = new Database(this)
// Setup database instance
this.database.ensureIndexes()
this.database.loadGraphPoints(config.graphDuration, () => {
this.database.loadRecords(callback)
})
}
handleReady () {
this.server.listen(config.site.ip, config.site.port)
// Allow individual modules to manage their own task scheduling
this.mojangUpdater.schedule()
this.pingController.schedule()
}
handleClientConnection = (client) => {
if (config.logToDatabase) {
client.on('requestHistoryGraph', () => {
// Send historical graphData built from all serverRegistrations
const graphData = {}
const graphPeaks = {}
this.serverRegistrations.forEach((serverRegistration) => {
graphData[serverRegistration.data.name] = serverRegistration.graphData
// Send current peak, if any
const graphPeak = serverRegistration.getGraphPeak()
if (graphPeak) {
graphPeaks[serverRegistration.data.name] = graphPeak
}
})
// Send current peaks, if any
// Emit peaks first since graphData may take a while to receive
if (Object.keys(graphPeaks).length > 0) {
client.emit('peaks', graphPeaks)
}
client.emit('historyGraph', graphData)
})
}
client.emit('setPublicConfig', (() => {
// Remap minecraftVersion entries into name values
const minecraftVersionNames = {}
Object.keys(minecraftVersions).forEach(function (key) {
minecraftVersionNames[key] = minecraftVersions[key].map(version => version.name)
})
// Send configuration data for rendering the page
return {
graphDuration: config.graphDuration,
servers: this.serverRegistrations.map(serverRegistration => serverRegistration.data),
minecraftVersions: minecraftVersionNames,
isGraphVisible: config.logToDatabase
}
})())
// Send last Mojang update, if any
this.mojangUpdater.sendLastUpdate(client)
// Send pingHistory of all ServerRegistrations
client.emit('add', this.serverRegistrations.map(serverRegistration => serverRegistration.getPingHistory()))
// Always send last
// This tells the frontend to do final processing and render
client.emit('syncComplete')
}
}
module.exports = App

@ -1,47 +1,105 @@
/**
* THIS IS LEGACY, UNMAINTAINED CODE
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
* USAGE IS NOT RECOMMENDED
*/
var util = require('./util');
const sqlite = require('sqlite3')
exports.setup = function() {
var sqlite = require('sqlite3');
class Database {
constructor (app) {
this._app = app
this._sql = new sqlite.Database('database.sql')
}
var db = new sqlite.Database('database.sql');
ensureIndexes () {
this._sql.serialize(() => {
this._sql.run('CREATE TABLE IF NOT EXISTS pings (timestamp BIGINT NOT NULL, ip TINYTEXT, playerCount MEDIUMINT)')
this._sql.run('CREATE INDEX IF NOT EXISTS ip_index ON pings (ip, playerCount)')
this._sql.run('CREATE INDEX IF NOT EXISTS timestamp_index on PINGS (timestamp)')
})
}
db.serialize(function() {
db.run('CREATE TABLE IF NOT EXISTS pings (timestamp BIGINT NOT NULL, ip TINYTEXT, playerCount MEDIUMINT)');
db.run('CREATE INDEX IF NOT EXISTS ip_index ON pings (ip, playerCount)');
db.run('CREATE INDEX IF NOT EXISTS timestamp_index on PINGS (timestamp)');
});
loadGraphPoints (graphDuration, callback) {
// Query recent pings
const endTime = new Date().getTime()
const startTime = endTime - graphDuration
exports.log = function(ip, timestamp, playerCount) {
var insertStatement = db.prepare('INSERT INTO pings (timestamp, ip, playerCount) VALUES (?, ?, ?)');
this.getRecentPings(startTime, endTime, pingData => {
const graphPointsByIp = []
db.serialize(function() {
insertStatement.run(timestamp, ip, playerCount);
});
for (const row of pingData) {
// Avoid loading outdated records
// This shouldn't happen and is mostly a sanity measure
if (startTime - row.timestamp <= graphDuration) {
// Load into temporary array
// This will be culled prior to being pushed to the serverRegistration
let graphPoints = graphPointsByIp[row.ip]
if (!graphPoints) {
graphPoints = graphPointsByIp[row.ip] = []
}
insertStatement.finalize();
};
graphPoints.push([row.timestamp, row.playerCount])
}
}
exports.getTotalRecord = function(ip, callback) {
db.all("SELECT MAX(playerCount), timestamp FROM pings WHERE ip = ?", [
ip
], function(err, data) {
callback(data[0]['MAX(playerCount)'], data[0]['timestamp']);
});
};
Object.keys(graphPointsByIp).forEach(ip => {
// Match IPs to serverRegistration object
for (const serverRegistration of this._app.serverRegistrations) {
if (serverRegistration.data.ip === ip) {
const graphPoints = graphPointsByIp[ip]
exports.queryPings = function(duration, callback) {
var currentTime = util.getCurrentTimeMs();
// Push the data into the instance and cull if needed
serverRegistration.loadGraphPoints(graphPoints)
db.all("SELECT * FROM pings WHERE timestamp >= ? AND timestamp <= ?", [
currentTime - duration,
currentTime
], function(err, data) {
callback(data);
});
};
};
break
}
}
})
callback()
})
}
loadRecords (callback) {
let completedTasks = 0
this._app.serverRegistrations.forEach(serverRegistration => {
// Find graphPeaks
// This pre-computes the values prior to clients connecting
serverRegistration.findNewGraphPeak()
// Query recordData
// When complete increment completeTasks to know when complete
this.getRecord(serverRegistration.data.ip, (playerCount, timestamp) => {
serverRegistration.recordData = {
playerCount: playerCount,
timestamp: timestamp
}
// Check if completedTasks hit the finish value
// Fire callback since #readyDatabase is complete
if (++completedTasks === this._app.serverRegistrations.length) {
callback()
}
})
})
}
getRecentPings (startTime, endTime, callback) {
this._sql.all('SELECT * FROM pings WHERE timestamp >= ? AND timestamp <= ?', [
startTime,
endTime
], (_, data) => callback(data))
}
getRecord (ip, callback) {
this._sql.all('SELECT MAX(playerCount), timestamp FROM pings WHERE ip = ?', [
ip
], (_, data) => callback(data[0]['MAX(playerCount)'], data[0].timestamp))
}
insertPing (ip, timestamp, playerCount) {
const statement = this._sql.prepare('INSERT INTO pings (timestamp, ip, playerCount) VALUES (?, ?, ?)')
this._sql.serialize(() => {
statement.run(timestamp, ip, playerCount)
})
statement.finalize()
}
}
module.exports = Database

@ -1,23 +1,17 @@
/**
* THIS IS LEGACY, UNMAINTAINED CODE
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
* USAGE IS NOT RECOMMENDED
*/
var winston = require('winston');
const winston = require('winston')
winston.remove(winston.transports.Console);
winston.remove(winston.transports.Console)
winston.add(winston.transports.File, {
filename: 'minetrack.log'
});
filename: 'minetrack.log'
})
winston.add(winston.transports.Console, {
'timestamp': function() {
var date = new Date();
timestamp: () => {
const date = new Date()
return date.toLocaleTimeString() + ' ' + date.getDate() + '/' + (date.getMonth() + 1) + '/' + date.getFullYear().toString().substring(2, 4)
},
colorize: true
})
return date.toLocaleTimeString() + ' ' + date.getDate() + '/' + (date.getMonth() + 1) + '/' + date.getFullYear().toString().substring(2, 4);
},
'colorize': true
});
module.exports = winston;
module.exports = winston

96
lib/mojang.js Normal file

@ -0,0 +1,96 @@
const request = require('request')
const logger = require('./logger')
const config = require('../config')
const SERVICE_URL_LOOKUP = {
'sessionserver.mojang.com': 'Sessions',
'authserver.mojang.com': 'Auth',
'textures.minecraft.net': 'Skins',
'api.mojang.com': 'API'
}
const TITLE_BY_MOJANG_COLOR = {
red: 'Offline',
yellow: 'Unstable',
green: 'Online'
}
class MojangUpdater {
constructor (app) {
this._app = app
}
schedule () {
setInterval(this.updateServices, config.rates.updateMojangStatus)
this.updateServices()
}
updateServices = () => {
request({
uri: 'https://status.mojang.com/check',
method: 'GET',
timeout: config.rates.mojangStatusTimeout
}, (err, _, body) => {
if (err) {
logger.log('error', 'Failed to update Mojang services: %s', err.message)
// Set all services to offline
// This may be incorrect, but if mojang.com is offline, it would never otherwise be reflected
Object.keys(SERVICE_URL_LOOKUP).forEach(url => {
this.handleServiceUpdate(url, 'red')
})
this.pushUpdate()
} else {
try {
JSON.parse(body).forEach(service => {
// Each service is formatted as an object with the 0 key being the URL
const url = Object.keys(service)[0]
this.handleServiceUpdate(url, service[url])
})
} catch (err) {
logger.log('error', 'Failed to parse Mojang response: %s', err.message)
}
this.pushUpdate()
}
})
}
pushUpdate () {
// Only fire callback when previous state is modified
if (this._hasUpdated) {
this._hasUpdated = false
this._app.server.broadcast('updateMojangServices', this._services)
}
}
sendLastUpdate (client) {
if (this._services) {
client.emit('updateMojangServices', this._services)
}
}
handleServiceUpdate (url, color) {
const service = SERVICE_URL_LOOKUP[url]
if (service) {
const requiredTitle = TITLE_BY_MOJANG_COLOR[color]
if (!this._services) {
this._services = {}
}
if (this._services[service] !== requiredTitle) {
this._services[service] = requiredTitle
this._hasUpdated = true
}
}
}
}
module.exports = MojangUpdater

@ -1,85 +0,0 @@
/**
* THIS IS LEGACY, UNMAINTAINED CODE
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
* USAGE IS NOT RECOMMENDED
*/
var request = require('request');
var logger = require('./logger');
var util = require('./util');
var serviceNameLookup = {
'sessionserver.mojang.com': 'Sessions',
'authserver.mojang.com': 'Auth',
'textures.minecraft.net': 'Skins',
'api.mojang.com': 'API'
};
var serviceStates = {
// Lazy populated.
};
function updateService(name, status) {
// Only update if we need to.
if (!(name in serviceStates) || serviceStates[name].status !== status) {
var newEntry = {
name: serviceNameLookup[name], // Send the clean name, not the URL.
status: status
};
// If it's an outage, track when it started.
if (status === 'yellow'|| status === 'red') {
newEntry.startTime = util.getCurrentTimeMs();
}
// Generate a nice title from the color.
if (status === 'green') {
newEntry.title = 'Online';
} else if (status === 'yellow') {
newEntry.title = 'Unstable';
} else if (status === 'red') {
newEntry.title = 'Offline';
} else {
throw new Error('Unknown Mojang status: ' + status);
}
// Wipe the old status in favor of the new one.
serviceStates[name] = newEntry;
}
}
exports.update = function(timeout) {
request({
uri: 'http://status.mojang.com/check',
method: 'GET',
timeout: timeout
}, function(err, res, body) {
if (err) {
logger.log('error', 'Failed to update Mojang services: %s', JSON.stringify(err));
} else {
try {
body = JSON.parse(body);
for (var i = 0; i < body.length; i++) {
var service = body[i];
var name = Object.keys(service)[0]; // Because they return an array of object, we have to do this :(
// If it's not in the lookup, we don't care about it.
if (name in serviceNameLookup) {
updateService(name, service[name]);
}
}
logger.log('debug', 'Updated Mojang services: %s', JSON.stringify(serviceStates));
} catch(err) {
// Catch anything weird that can happen, since things probably will.
logger.log('error', 'Failed to parse Mojang\'s response: %s', JSON.stringify(err));
}
}
});
};
exports.toMessage = function() {
// This is what we send to the clients.
return serviceStates;
};

@ -1,85 +1,143 @@
/**
* THIS IS LEGACY, UNMAINTAINED CODE
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
* USAGE IS NOT RECOMMENDED
*/
var mcpe_ping = require('mcpe-ping-fixed');
var mcpc_ping = require('mc-ping-updated');
const dns = require('dns')
var logger = require('./logger');
var util = require('./util');
const minecraftJavaPing = require('mc-ping-updated')
const minecraftBedrockPing = require('mcpe-ping-fixed')
// This is a wrapper function for mc-ping-updated, mainly used to convert the data structure of the result.
function pingMinecraftPC(host, port, timeout, callback, version) {
var startTime = util.getCurrentTimeMs();
const logger = require('./logger')
mcpc_ping(host, port, function(err, res) {
if (err) {
callback(err, null);
} else {
// Remap our JSON into our custom structure.
var favicon;
const config = require('../config')
// Ensure the returned favicon is a data URI
if (res.favicon && res.favicon.indexOf('data:image/') === 0) {
favicon = res.favicon;
}
function ping (host, port, type, timeout, callback, version) {
switch (type) {
case 'PC':
unfurlSrv(host, port, (host, port) => {
minecraftJavaPing(host, port || 25565, (err, res) => {
if (err) {
callback(err)
} else {
const payload = {
players: {
online: capPlayerCount(host, parseInt(res.players.online))
},
version: parseInt(res.version.protocol)
}
callback(null, {
players: {
online: capPlayerCount(host, parseInt(res.players.online)),
max: parseInt(res.players.max)
},
version: parseInt(res.version.protocol),
latency: util.getCurrentTimeMs() - startTime,
favicon: favicon
});
}
}, timeout, version);
// Ensure the returned favicon is a data URI
if (res.favicon && res.favicon.startsWith('data:image/')) {
payload.favicon = res.favicon
}
callback(null, payload)
}
}, timeout, version)
})
break
case 'PE':
minecraftBedrockPing(host, port || 19132, (err, res) => {
if (err) {
callback(err)
} else {
callback(null, {
players: {
online: capPlayerCount(host, parseInt(res.currentPlayers))
}
})
}
}, timeout)
break
default:
throw new Error('Unsupported type: ' + type)
}
}
// This is a wrapper function for mcpe-ping, mainly used to convert the data structure of the result.
function pingMinecraftPE(host, port, timeout, callback) {
var startTime = util.getCurrentTimeMs();
mcpe_ping(host, port || 19132, function(err, res) {
if (err) {
callback(err, null);
} else {
// Remap our JSON into our custom structure.
callback(err, {
players: {
online: capPlayerCount(host, parseInt(res.currentPlayers)),
max: parseInt(res.maxPlayers)
},
latency: util.getCurrentTimeMs() - startTime
});
}
}, timeout);
function unfurlSrv (hostname, port, callback) {
dns.resolveSrv('_minecraft._tcp.' + hostname, (_, records) => {
if (!records || records.length < 1) {
callback(hostname, port)
} else {
callback(records[0].name, records[0].port)
}
})
}
// player count can be up to 1^32-1, which is a massive scale and destroys browser performance when rendering graphs
// Artificially cap and warn to prevent propogating garbage
function capPlayerCount(host, playerCount) {
const maxPlayerCount = 250000;
if (playerCount !== Math.min(playerCount, maxPlayerCount)) {
logger.log('warn', '%s returned a player count of %d, Minetrack has capped it to %d to prevent browser performance issues with graph rendering. If this is in error, please edit maxPlayerCount in ping.js!', host, playerCount, maxPlayerCount);
return maxPlayerCount;
} else if (playerCount !== Math.max(playerCount, 0)) {
logger.log('warn', '%s returned an invalid player count of %d, setting to 0.', host, playerCount);
return 0;
}
return playerCount;
function capPlayerCount (host, playerCount) {
const maxPlayerCount = 250000
if (playerCount !== Math.min(playerCount, maxPlayerCount)) {
logger.log('warn', '%s returned a player count of %d, Minetrack has capped it to %d to prevent browser performance issues with graph rendering. If this is in error, please edit maxPlayerCount in ping.js!', host, playerCount, maxPlayerCount)
return maxPlayerCount
} else if (playerCount !== Math.max(playerCount, 0)) {
logger.log('warn', '%s returned an invalid player count of %d, setting to 0.', host, playerCount)
return 0
}
return playerCount
}
exports.ping = function(host, port, type, timeout, callback, version) {
if (type === 'PC') {
util.unfurlSRV(host, port, function(host, port){
pingMinecraftPC(host, port || 25565, timeout, callback, version);
})
} else if (type === 'PE') {
pingMinecraftPE(host, port || 19132, timeout, callback);
} else {
throw new Error('Unsupported type: ' + type);
}
};
class PingController {
constructor (app) {
this._app = app
}
schedule () {
setInterval(this.pingAll, config.rates.pingAll)
this.pingAll()
}
pingAll = () => {
for (const serverRegistration of this._app.serverRegistrations) {
const version = serverRegistration.getNextProtocolVersion()
ping(serverRegistration.data.ip, serverRegistration.data.port, serverRegistration.data.type, config.rates.connectTimeout, (err, resp) => {
if (err) {
logger.log('error', 'Failed to ping %s: %s', serverRegistration.data.ip, err.message)
}
this.handlePing(serverRegistration, resp, err, version)
}, version.protocolId)
}
}
handlePing (serverRegistration, resp, err, version) {
const timestamp = new Date().getTime()
this._app.server.broadcast('update', serverRegistration.getUpdate(timestamp, resp, err, version))
serverRegistration.addPing(timestamp, resp)
if (config.logToDatabase) {
const playerCount = resp ? resp.players.online : 0
// Log to database
this._app.database.insertPing(serverRegistration.data.ip, timestamp, playerCount)
if (serverRegistration.addGraphPoint(resp !== undefined, playerCount, timestamp)) {
this._app.server.broadcast('updateHistoryGraph', {
name: serverRegistration.data.name,
playerCount: playerCount,
timestamp: timestamp
})
}
// Update calculated graph peak regardless if the graph is being updated
// This can cause a (harmless) desync between live and stored data, but it allows it to be more accurate for long surviving processes
if (serverRegistration.findNewGraphPeak()) {
const graphPeak = serverRegistration.getGraphPeak()
this._app.server.broadcast('updatePeak', {
name: serverRegistration.data.name,
playerCount: graphPeak.playerCount,
timestamp: graphPeak.timestamp
})
}
}
}
}
module.exports = PingController

@ -1,32 +1,64 @@
/**
* THIS IS LEGACY, UNMAINTAINED CODE
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
* USAGE IS NOT RECOMMENDED
*/
const http = require('http')
const io = require('socket.io')
const finalHandler = require('finalhandler')
const finalHttpHandler = require('finalhandler')
const serveStatic = require('serve-static')
const io = require('socket.io')
const util = require('./util')
const logger = require('./logger')
const config = require('../config.json')
const distHandler = serveStatic('dist/')
const faviconsHandler = serveStatic('favicons/')
function onRequest (req, res) {
logger.log('info', '%s requested: %s', util.getRemoteAddr(req), req.url)
distHandler(req, res, function () {
faviconsHandler(req, res, finalHandler(req, res))
})
function getRemoteAddr (req) {
return req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddress
}
exports.start = function () {
const server = http.createServer(onRequest)
server.listen(config.site.port, config.site.ip)
exports.io = io.listen(server)
logger.log('info', 'Started on %s:%d', config.site.ip, config.site.port)
class Server {
constructor (clientSocketHandler) {
this._clientSocketHandler = clientSocketHandler
this._connectedSockets = 0
this._http = http.createServer(this.handleHttpRequest)
this._distServeStatic = serveStatic('dist/')
this._faviconsServeStatic = serveStatic('favicons/')
}
listen (host, port) {
this._http.listen(port, host)
this._io = io.listen(this._http)
this._io.on('connect', this.handleClientSocket)
logger.log('info', 'Started on %s:%d', host, port)
}
broadcast (event, payload) {
this._io.sockets.emit(event, payload)
}
handleHttpRequest = (req, res) => {
logger.log('info', '%s requested: %s', getRemoteAddr(req), req.url)
// Attempt to handle req using distServeStatic, otherwise fail over to faviconServeStatic
// If faviconServeStatic fails, pass to finalHttpHandler to terminate
this._distServeStatic(req, res, () => {
this._faviconsServeStatic(req, res, finalHttpHandler(req, res))
})
}
handleClientSocket = (client) => {
this._connectedSockets++
logger.log('info', '%s connected, total clients: %d', getRemoteAddr(client.request), this._connectedSockets)
// Bind disconnect event for logging
client.on('disconnect', () => {
this._connectedSockets--
logger.log('info', '%s disconnected, total clients: %d', getRemoteAddr(client.request), this._connectedSockets)
})
// Pass client off to proxy handler
this._clientSocketHandler(client)
}
}
module.exports = Server

279
lib/servers.js Normal file

@ -0,0 +1,279 @@
const config = require('../config')
const minecraftVersions = require('../minecraft_versions')
class ServerRegistration {
lastFavicon
versions = []
recordData
graphData = []
constructor (data) {
this.data = data
this._pingHistory = []
this._hasInitialRecordData = false
}
getUpdate (timestamp, resp, err, version) {
const update = {
info: {
name: this.data.name,
timestamp: timestamp
}
}
if (resp) {
if (resp.version && this.updateProtocolVersionCompat(resp.version, version.protocolId, version.protocolIndex)) {
// Append an updated version listing
update.versions = this.versions
}
// Validate that we have logToDatabase enabled otherwise in memory pings
// will create a record that's only valid for the runtime duration.
if (config.logToDatabase && (!this._hasInitialRecordData || !this.recordData || resp.players.online > this.recordData.playerCount)) {
// For instances with existing data, recordData will be defined at boot
// This causes initial updates to NOT include recordData since it hasn't changed
// This flag force includes it to avoid "late loading"
this._hasInitialRecordData = true
this.recordData = {
playerCount: resp.players.online,
timestamp: timestamp
}
// Append an updated recordData
update.recordData = this.recordData
}
// Compare against this.data.favicon to support favicon overrides
const newFavicon = resp.favicon || this.data.favicon
if (this.updateFavicon(newFavicon)) {
// Append an updated favicon
update.favicon = newFavicon
}
// Append a result object
// This filters out unwanted data from resp
update.result = {
players: resp.players
}
} else if (err) {
// Append a filtered copy of err
// This ensures any unintended data is not leaked
update.error = this.filterError(err)
}
return update
}
addPing (timestamp, resp) {
const ping = {
timestamp: timestamp
}
if (resp) {
// Append a result object
// This filters out unwanted data from resp
ping.result = {
players: {
online: resp.players.online
}
}
}
this._pingHistory.push(ping)
// Trim pingHistory to avoid memory leaks
if (this._pingHistory.length > 72) {
this._pingHistory.shift()
}
}
getPingHistory () {
if (this._pingHistory.length > 0) {
const pingHistory = []
for (let i = 0; i < this._pingHistory.length - 1; i++) {
pingHistory[i] = this._pingHistory[i]
}
// Insert the latest update manually into the array
// This is a mutated copy of the last update to contain live metadata
// The metadata is used by the frontend for rendering
const lastPing = this._pingHistory[this._pingHistory.length - 1]
const payload = {
info: {
name: this.data.name
},
timestamp: lastPing.timestamp,
versions: this.versions,
recordData: this.recordData,
favicon: this.lastFavicon
}
// Conditionally append to avoid defining fields with undefined values
if (lastPing.result) {
payload.result = lastPing.result
} else if (lastPing.error) {
payload.error = lastPing.error
}
// Insert the reconstructed update as the last entry
// pingHistory is already sorted during its copy from _pingHistory
pingHistory.push(payload)
return pingHistory
}
return [{
error: {
message: 'Waiting...',
placeholder: true
},
timestamp: new Date().getTime(),
info: {
name: this.data.name
},
recordData: this.recordData
}]
}
loadGraphPoints (points) {
// Filter pings so each result is a minute apart
const minutePoints = []
let lastTimestamp = 0
for (const point of points) {
// 0 is the index of the timestamp
if (point[0] - lastTimestamp >= 60 * 1000) {
// This check tries to smooth out randomly dropped pings
// By default only filter pings that are online (playerCount > 0)
// This will keep looking forward until it finds a ping that is online
// If it can't find one within a reasonable timeframe, it will select a failed ping
if (point[0] - lastTimestamp >= 120 * 1000 || point[1] > 0) {
minutePoints.push(point)
lastTimestamp = point[0]
}
}
}
if (minutePoints.length > 0) {
this.graphData = minutePoints
// Select the last entry to use for lastGraphDataPush
this._lastGraphDataPush = minutePoints[minutePoints.length - 1][0]
}
}
addGraphPoint (isSuccess, playerCount, timestamp) {
// If the ping failed, then to avoid destroying the graph, ignore it
// However if it's been too long since the last successful ping, push it anyways
if (this._lastGraphDataPush) {
const timeSince = timestamp - this._lastGraphDataPush
if ((isSuccess && timeSince < 60 * 1000) || (!isSuccess && timeSince < 70 * 1000)) {
return false
}
}
this.graphData.push([timestamp, playerCount])
this._lastGraphDataPush = timestamp
// Trim old graphPoints according to graphDuration
for (let i = 1; i < this.graphData.length; i++) {
// Find a break point where i - 1 is too old and i is new
if (timestamp - this.graphData[i - 1][0] > config.graphDuration && timestamp - this.graphData[i] <= config.graphDuration) {
this.graphData.splice(0, i)
break
}
}
return true
}
findNewGraphPeak () {
let index = -1
for (let i = 0; i < this.graphData.length; i++) {
const point = this.graphData[i]
if (index === -1 || point[1] > this.graphData[index][1]) {
index = i
}
}
if (index >= 0) {
const lastGraphPeakIndex = this._graphPeakIndex
this._graphPeakIndex = index
return index !== lastGraphPeakIndex
} else {
this._graphPeakIndex = undefined
return false
}
}
getGraphPeak () {
if (this._graphPeakIndex === undefined) {
return
}
const graphPeak = this.graphData[this._graphPeakIndex]
return {
playerCount: graphPeak[1],
timestamp: graphPeak[0]
}
}
updateFavicon (favicon) {
if (favicon && favicon !== this.lastFavicon) {
this.lastFavicon = favicon
return true
}
return false
}
updateProtocolVersionCompat (incomingId, outgoingId, protocolIndex) {
// If the result version matches the attempted version, the version is supported
const isSuccess = incomingId === outgoingId
const indexOf = this.versions.indexOf(protocolIndex)
// Test indexOf to avoid inserting previously recorded protocolIndex values
if (isSuccess && indexOf < 0) {
this.versions.push(protocolIndex)
// Sort versions in ascending order
// This matches protocol ids to Minecraft versions release order
this.versions.sort((a, b) => a - b)
return true
} else if (!isSuccess && indexOf >= 0) {
this.versions.splice(indexOf, 1)
return true
}
return false
}
getNextProtocolVersion () {
const protocolVersions = minecraftVersions[this.data.type]
if (typeof this._nextProtocolIndex === 'undefined' || this._nextProtocolIndex + 1 >= protocolVersions.length) {
this._nextProtocolIndex = 0
} else {
this._nextProtocolIndex++
}
return {
protocolId: protocolVersions[this._nextProtocolIndex].protocolId,
protocolIndex: this._nextProtocolIndex
}
}
filterError (err) {
// Attempt to match to the first possible value
for (const key of ['message', 'description', 'errno']) {
if (err[key]) {
return {
message: err[key]
}
}
}
return {
message: 'Unknown error'
}
}
}
module.exports = ServerRegistration

@ -1,174 +0,0 @@
/**
* THIS IS LEGACY, UNMAINTAINED CODE
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
* USAGE IS NOT RECOMMENDED
*/
var logger = require('./logger');
var dns = require('dns');
var config = require('../config.json');
var servers = require('../servers.json');
var serverNameLookup = [];
// Finds a server in servers.json with a matching IP.
// If it finds one, it caches the result for faster future lookups.
function getServerNameByIp(ip) {
var lookupName = serverNameLookup[ip];
if (lookupName) {
return lookupName;
}
for (var i = 0; i < servers.length; i++) {
var entry = servers[i];
if (entry.ip === ip) {
serverNameLookup[entry.ip] = entry.name;
return entry.name;
}
}
}
// Returns a list of configured server IPs from servers.json
function getServerIps() {
var ips = [];
for (var i = 0; i < servers.length; i++) {
ips.push(servers[i].ip);
}
return ips;
}
// This method is a monstrosity.
// Since we loaded ALL pings from the database, we need to filter out the pings so each entry is a minute apart.
// This is done by iterating over the list, since the time between each ping can be completely arbitrary.
function trimUselessPings(data) {
var keys = Object.keys(data);
for (var i = 0; i < keys.length; i++) {
var listing = data[keys[i]];
var lastTimestamp = 0;
var filteredListing = [];
for (var x = 0; x < listing.length; x++) {
var entry = listing[x];
// 0 is the index of the timestamp.
// See the convertPingsToGraph method.
if (entry[0] - lastTimestamp >= 60 * 1000) {
// This second check tries to smooth out randomly dropped pings.
// By default we only want entries that are online (playerCount > 0).
// This way we'll keep looking forward until we find one that is online.
// However if we can't find one within a reasonable timeframe, select the sucky one.
if (entry[0] - lastTimestamp >= 120 * 1000 || entry[1] > 0) {
filteredListing.push(entry);
lastTimestamp = entry[0];
}
}
}
data[keys[i]] = filteredListing;
}
}
exports.trimOldPings = function(data) {
var keys = Object.keys(data);
var timeMs = exports.getCurrentTimeMs();
for (var x = 0; x < keys.length; x++) {
var listing = data[keys[x]];
var toSplice = [];
for (var i = 0; i < listing.length; i++) {
var entry = listing[i];
if (timeMs - entry[0] > config.graphDuration) {
toSplice.push(i);
}
}
for (var i = 0; i < toSplice.length; i++) {
listing.splice(toSplice[i], 1);
}
}
}
exports.getCurrentTimeMs = function() {
return new Date().getTime();
};
exports.stringToColor = function(base) {
var hash;
for (var i = base.length - 1, hash = 0; i >= 0; i--) {
hash = base.charCodeAt(i) + ((hash << 5) - hash);
}
color = Math.floor(Math.abs((Math.sin(hash) * 10000) % 1 * 16777216)).toString(16);
return '#' + Array(6 - color.length + 1).join('0') + color;
}
exports.setIntervalNoDelay = function(func, delay) {
var task = setInterval(func, delay);
func();
return task;
};
exports.convertServerHistory = function(sqlData) {
var serverIps = getServerIps();
var graphData = {};
var startTime = exports.getCurrentTimeMs();
for (var i = 0; i < sqlData.length; i++) {
var entry = sqlData[i];
if (serverIps.indexOf(entry.ip) === -1) continue;
var name = getServerNameByIp(entry.ip);
if (!graphData[name]) graphData[name] = [];
graphData[name].push([entry.timestamp, entry.playerCount]);
}
// Break it into minutes.
trimUselessPings(graphData);
// Drop old data.
exports.trimOldPings(graphData);
logger.info('Parsed ' + sqlData.length + ' ping records in ' + (exports.getCurrentTimeMs() - startTime) + 'ms');
return graphData;
};
/**
* Attempts to resolve Minecraft PC SRV records from DNS, otherwise falling back to the old hostname.
*
* @param hostname hostname to check
* @param port port to pass to callback if required
* @param callback function with a hostname and port parameter
*/
exports.unfurlSRV = function(hostname, port, callback) {
dns.resolveSrv("_minecraft._tcp."+hostname, function (err, records) {
if(!records||records.length<=0) {
callback(hostname, port);
return;
}
callback(records[0].name, records[0].port);
})
};
exports.getRemoteAddr = function(req) {
let remoteAddress = req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddress;
return remoteAddress;
};

36
main.js Normal file

@ -0,0 +1,36 @@
const App = require('./lib/app')
const ServerRegistration = require('./lib/servers')
const logger = require('./lib/logger')
const config = require('./config')
const servers = require('./servers')
const app = new App()
servers.forEach(server => {
// Assign a generated color for each servers.json entry if not manually defined
// These will be passed to the frontend for use in rendering
if (!server.color) {
let hash = 0
for (let i = server.name.length - 1; i >= 0; i--) {
hash = server.name.charCodeAt(i) + ((hash << 5) - hash)
}
const color = Math.floor(Math.abs((Math.sin(hash) * 10000) % 1 * 16777216)).toString(16)
server.color = '#' + Array(6 - color.length + 1).join('0') + color
}
// Init a ServerRegistration instance of each entry in servers.json
app.serverRegistrations.push(new ServerRegistration(server))
})
if (!config.logToDatabase) {
logger.log('warn', 'Database logging is not enabled. You can enable it by setting "logToDatabase" to true in config.json. This requires sqlite3 to be installed.')
app.handleReady()
} else {
app.loadDatabase(() => {
app.handleReady()
})
}

@ -1,8 +1,8 @@
{
"name": "minetrack",
"version": "5.0.0",
"version": "5.1.0",
"description": "A Minecraft server tracker that lets you focus on the basics.",
"main": "app.js",
"main": "main.js",
"dependencies": {
"finalhandler": "^1.1.2",
"mc-ping-updated": "0.1.1",

@ -1,5 +1,5 @@
while true;
do
node app.js
node main.js
sleep 5
done