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:
parent
9eda8d6bdb
commit
4d13965e6b
392
app.js
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)
|
const serverRegistration = app.serverRegistry.getServerRegistration(data.name)
|
||||||
|
|
||||||
if (serverRegistration) {
|
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
|
// Only redraw the graph if not mutating hidden data
|
||||||
if (serverRegistration.isVisible) {
|
if (serverRegistration.isVisible) {
|
||||||
@ -94,7 +94,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
})
|
})
|
||||||
|
|
||||||
socket.on('updateMojangServices', function (data) {
|
socket.on('updateMojangServices', function (data) {
|
||||||
Object.values(data).forEach(app.mojangUpdater.updateServiceStatus)
|
app.mojangUpdater.updateStatus(data)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.on('setPublicConfig', function (data) {
|
socket.on('setPublicConfig', function (data) {
|
||||||
@ -125,7 +125,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
const serverRegistration = app.serverRegistry.getServerRegistration(data.name)
|
const serverRegistration = app.serverRegistry.getServerRegistration(data.name)
|
||||||
|
|
||||||
if (serverRegistration) {
|
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)
|
const serverRegistration = app.serverRegistry.getServerRegistration(serverName)
|
||||||
|
|
||||||
if (serverRegistration) {
|
if (serverRegistration) {
|
||||||
const graphData = data[serverName]
|
const graphPeak = data[serverName]
|
||||||
|
|
||||||
// [0] and [1] indexes correspond to flot.js' graphing data structure
|
serverRegistration.updateServerPeak(graphPeak.timestamp, graphPeak.playerCount)
|
||||||
serverRegistration.updateServerPeak(graphData[0], graphData[1])
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
const MOJANG_STATUS_BASE_CLASS = 'header-button header-button-group'
|
const MOJANG_STATUS_BASE_CLASS = 'header-button header-button-group'
|
||||||
|
|
||||||
export class MojangUpdater {
|
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
|
// 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_' + name).setAttribute('class', MOJANG_STATUS_BASE_CLASS + ' mojang-status-' + title.toLowerCase())
|
||||||
document.getElementById('mojang-status-text_' + status.name).innerText = status.title
|
document.getElementById('mojang-status-text_' + name).innerText = title
|
||||||
}
|
}
|
||||||
|
|
||||||
reset () {
|
reset () {
|
||||||
|
@ -204,7 +204,7 @@ export class ServerRegistration {
|
|||||||
const versionsElement = document.getElementById('version_' + this.serverId)
|
const versionsElement = document.getElementById('version_' + this.serverId)
|
||||||
|
|
||||||
versionsElement.style.display = 'block'
|
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
|
// Compare against a cached value to avoid empty updates
|
||||||
@ -237,8 +237,7 @@ export class ServerRegistration {
|
|||||||
playerCountLabelElement.style.display = 'none'
|
playerCountLabelElement.style.display = 'none'
|
||||||
errorElement.style.display = 'block'
|
errorElement.style.display = 'block'
|
||||||
|
|
||||||
// Attempt to find an error cause from documented options
|
errorElement.innerText = ping.error.message
|
||||||
errorElement.innerText = ping.error.description || ping.error.errno || 'Unknown error'
|
|
||||||
} else if (ping.result) {
|
} else if (ping.result) {
|
||||||
// Ensure the player-count element is visible and hide the error element
|
// Ensure the player-count element is visible and hide the error element
|
||||||
playerCountLabelElement.style.display = 'block'
|
playerCountLabelElement.style.display = 'block'
|
||||||
|
@ -64,16 +64,14 @@ export function formatMinecraftVersions (versions, knownVersions) {
|
|||||||
const versionGroups = []
|
const versionGroups = []
|
||||||
|
|
||||||
for (let i = 0; i < versions.length; i++) {
|
for (let i = 0; i < versions.length; i++) {
|
||||||
const versionIndex = versions[i]
|
|
||||||
|
|
||||||
// Look for value mismatch between the previous index
|
// Look for value mismatch between the previous index
|
||||||
// Require i > 0 since lastVersionIndex is undefined for i === 0
|
// 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)
|
versionGroups.push(currentVersionGroup)
|
||||||
currentVersionGroup = []
|
currentVersionGroup = []
|
||||||
}
|
}
|
||||||
|
|
||||||
currentVersionGroup.push(versionIndex)
|
currentVersionGroup.push(versions[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the last versionGroup is always pushed
|
// Ensure the last versionGroup is always pushed
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
"ip": "0.0.0.0"
|
"ip": "0.0.0.0"
|
||||||
},
|
},
|
||||||
"rates": {
|
"rates": {
|
||||||
"upateMojangStatus": 5000,
|
"updateMojangStatus": 5000,
|
||||||
"mojangStatusTimeout": 3500,
|
"mojangStatusTimeout": 3500,
|
||||||
"pingAll": 3000,
|
"pingAll": 3000,
|
||||||
"connectTimeout": 2500
|
"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!
|
- New logo!
|
||||||
- Completely rebuilt the frontend's Javascript (heavy optimizations and cleanup!)
|
- Completely rebuilt the frontend's Javascript (heavy optimizations and cleanup!)
|
||||||
- Adds a button for mobile devices to manually request the historical graph
|
- Adds a button for mobile devices to manually request the historical graph
|
||||||
|
92
lib/app.js
Normal file
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
|
132
lib/database.js
132
lib/database.js
@ -1,47 +1,105 @@
|
|||||||
/**
|
const sqlite = require('sqlite3')
|
||||||
* THIS IS LEGACY, UNMAINTAINED CODE
|
|
||||||
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
|
|
||||||
* USAGE IS NOT RECOMMENDED
|
|
||||||
*/
|
|
||||||
var util = require('./util');
|
|
||||||
|
|
||||||
exports.setup = function() {
|
class Database {
|
||||||
var sqlite = require('sqlite3');
|
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() {
|
loadGraphPoints (graphDuration, callback) {
|
||||||
db.run('CREATE TABLE IF NOT EXISTS pings (timestamp BIGINT NOT NULL, ip TINYTEXT, playerCount MEDIUMINT)');
|
// Query recent pings
|
||||||
db.run('CREATE INDEX IF NOT EXISTS ip_index ON pings (ip, playerCount)');
|
const endTime = new Date().getTime()
|
||||||
db.run('CREATE INDEX IF NOT EXISTS timestamp_index on PINGS (timestamp)');
|
const startTime = endTime - graphDuration
|
||||||
});
|
|
||||||
|
|
||||||
exports.log = function(ip, timestamp, playerCount) {
|
this.getRecentPings(startTime, endTime, pingData => {
|
||||||
var insertStatement = db.prepare('INSERT INTO pings (timestamp, ip, playerCount) VALUES (?, ?, ?)');
|
const graphPointsByIp = []
|
||||||
|
|
||||||
db.serialize(function() {
|
for (const row of pingData) {
|
||||||
insertStatement.run(timestamp, ip, playerCount);
|
// 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) {
|
Object.keys(graphPointsByIp).forEach(ip => {
|
||||||
db.all("SELECT MAX(playerCount), timestamp FROM pings WHERE ip = ?", [
|
// Match IPs to serverRegistration object
|
||||||
|
for (const serverRegistration of this._app.serverRegistrations) {
|
||||||
|
if (serverRegistration.data.ip === ip) {
|
||||||
|
const graphPoints = graphPointsByIp[ip]
|
||||||
|
|
||||||
|
// Push the data into the instance and cull if needed
|
||||||
|
serverRegistration.loadGraphPoints(graphPoints)
|
||||||
|
|
||||||
|
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
|
ip
|
||||||
], function(err, data) {
|
], (_, data) => callback(data[0]['MAX(playerCount)'], data[0].timestamp))
|
||||||
callback(data[0]['MAX(playerCount)'], data[0]['timestamp']);
|
}
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.queryPings = function(duration, callback) {
|
insertPing (ip, timestamp, playerCount) {
|
||||||
var currentTime = util.getCurrentTimeMs();
|
const statement = this._sql.prepare('INSERT INTO pings (timestamp, ip, playerCount) VALUES (?, ?, ?)')
|
||||||
|
this._sql.serialize(() => {
|
||||||
|
statement.run(timestamp, ip, playerCount)
|
||||||
|
})
|
||||||
|
statement.finalize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
db.all("SELECT * FROM pings WHERE timestamp >= ? AND timestamp <= ?", [
|
module.exports = Database
|
||||||
currentTime - duration,
|
|
||||||
currentTime
|
|
||||||
], function(err, data) {
|
|
||||||
callback(data);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
@ -1,23 +1,17 @@
|
|||||||
/**
|
const winston = require('winston')
|
||||||
* THIS IS LEGACY, UNMAINTAINED CODE
|
|
||||||
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
|
|
||||||
* USAGE IS NOT RECOMMENDED
|
|
||||||
*/
|
|
||||||
var winston = require('winston');
|
|
||||||
|
|
||||||
winston.remove(winston.transports.Console);
|
winston.remove(winston.transports.Console)
|
||||||
|
|
||||||
winston.add(winston.transports.File, {
|
winston.add(winston.transports.File, {
|
||||||
filename: 'minetrack.log'
|
filename: 'minetrack.log'
|
||||||
});
|
})
|
||||||
|
|
||||||
winston.add(winston.transports.Console, {
|
winston.add(winston.transports.Console, {
|
||||||
'timestamp': function() {
|
timestamp: () => {
|
||||||
var date = new Date();
|
const date = new Date()
|
||||||
|
return date.toLocaleTimeString() + ' ' + date.getDate() + '/' + (date.getMonth() + 1) + '/' + date.getFullYear().toString().substring(2, 4)
|
||||||
return date.toLocaleTimeString() + ' ' + date.getDate() + '/' + (date.getMonth() + 1) + '/' + date.getFullYear().toString().substring(2, 4);
|
|
||||||
},
|
},
|
||||||
'colorize': true
|
colorize: true
|
||||||
});
|
})
|
||||||
|
|
||||||
module.exports = winston;
|
module.exports = winston
|
||||||
|
96
lib/mojang.js
Normal file
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;
|
|
||||||
};
|
|
176
lib/ping.js
176
lib/ping.js
@ -1,85 +1,143 @@
|
|||||||
/**
|
const dns = require('dns')
|
||||||
* 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');
|
|
||||||
|
|
||||||
var logger = require('./logger');
|
const minecraftJavaPing = require('mc-ping-updated')
|
||||||
var util = require('./util');
|
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.
|
const logger = require('./logger')
|
||||||
function pingMinecraftPC(host, port, timeout, callback, version) {
|
|
||||||
var startTime = util.getCurrentTimeMs();
|
|
||||||
|
|
||||||
mcpc_ping(host, port, function(err, res) {
|
const config = require('../config')
|
||||||
|
|
||||||
|
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) {
|
if (err) {
|
||||||
callback(err, null);
|
callback(err)
|
||||||
} else {
|
} else {
|
||||||
// Remap our JSON into our custom structure.
|
const payload = {
|
||||||
var favicon;
|
players: {
|
||||||
|
online: capPlayerCount(host, parseInt(res.players.online))
|
||||||
|
},
|
||||||
|
version: parseInt(res.version.protocol)
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure the returned favicon is a data URI
|
// Ensure the returned favicon is a data URI
|
||||||
if (res.favicon && res.favicon.indexOf('data:image/') === 0) {
|
if (res.favicon && res.favicon.startsWith('data:image/')) {
|
||||||
favicon = res.favicon;
|
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, {
|
callback(null, {
|
||||||
players: {
|
players: {
|
||||||
online: capPlayerCount(host, parseInt(res.players.online)),
|
online: capPlayerCount(host, parseInt(res.currentPlayers))
|
||||||
max: parseInt(res.players.max)
|
}
|
||||||
},
|
})
|
||||||
version: parseInt(res.version.protocol),
|
}
|
||||||
latency: util.getCurrentTimeMs() - startTime,
|
}, timeout)
|
||||||
favicon: favicon
|
break
|
||||||
});
|
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported type: ' + type)
|
||||||
}
|
}
|
||||||
}, timeout, version);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a wrapper function for mcpe-ping, mainly used to convert the data structure of the result.
|
function unfurlSrv (hostname, port, callback) {
|
||||||
function pingMinecraftPE(host, port, timeout, callback) {
|
dns.resolveSrv('_minecraft._tcp.' + hostname, (_, records) => {
|
||||||
var startTime = util.getCurrentTimeMs();
|
if (!records || records.length < 1) {
|
||||||
|
callback(hostname, port)
|
||||||
mcpe_ping(host, port || 19132, function(err, res) {
|
|
||||||
if (err) {
|
|
||||||
callback(err, null);
|
|
||||||
} else {
|
} else {
|
||||||
// Remap our JSON into our custom structure.
|
callback(records[0].name, records[0].port)
|
||||||
callback(err, {
|
|
||||||
players: {
|
|
||||||
online: capPlayerCount(host, parseInt(res.currentPlayers)),
|
|
||||||
max: parseInt(res.maxPlayers)
|
|
||||||
},
|
|
||||||
latency: util.getCurrentTimeMs() - startTime
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, timeout);
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// player count can be up to 1^32-1, which is a massive scale and destroys browser performance when rendering graphs
|
// 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
|
// Artificially cap and warn to prevent propogating garbage
|
||||||
function capPlayerCount(host, playerCount) {
|
function capPlayerCount (host, playerCount) {
|
||||||
const maxPlayerCount = 250000;
|
const maxPlayerCount = 250000
|
||||||
|
|
||||||
if (playerCount !== Math.min(playerCount, maxPlayerCount)) {
|
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);
|
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;
|
|
||||||
|
return maxPlayerCount
|
||||||
} else if (playerCount !== Math.max(playerCount, 0)) {
|
} else if (playerCount !== Math.max(playerCount, 0)) {
|
||||||
logger.log('warn', '%s returned an invalid player count of %d, setting to 0.', host, playerCount);
|
logger.log('warn', '%s returned an invalid player count of %d, setting to 0.', host, playerCount)
|
||||||
return 0;
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
return playerCount;
|
return playerCount
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.ping = function(host, port, type, timeout, callback, version) {
|
class PingController {
|
||||||
if (type === 'PC') {
|
constructor (app) {
|
||||||
util.unfurlSRV(host, port, function(host, port){
|
this._app = app
|
||||||
pingMinecraftPC(host, port || 25565, timeout, callback, version);
|
|
||||||
})
|
|
||||||
} else if (type === 'PE') {
|
|
||||||
pingMinecraftPE(host, port || 19132, timeout, callback);
|
|
||||||
} else {
|
|
||||||
throw new Error('Unsupported type: ' + type);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
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 http = require('http')
|
||||||
|
|
||||||
const io = require('socket.io')
|
const finalHttpHandler = require('finalhandler')
|
||||||
const finalHandler = require('finalhandler')
|
|
||||||
const serveStatic = require('serve-static')
|
const serveStatic = require('serve-static')
|
||||||
|
const io = require('socket.io')
|
||||||
|
|
||||||
const util = require('./util')
|
|
||||||
const logger = require('./logger')
|
const logger = require('./logger')
|
||||||
|
|
||||||
const config = require('../config.json')
|
function getRemoteAddr (req) {
|
||||||
|
return req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddress
|
||||||
|
}
|
||||||
|
|
||||||
const distHandler = serveStatic('dist/')
|
class Server {
|
||||||
const faviconsHandler = serveStatic('favicons/')
|
constructor (clientSocketHandler) {
|
||||||
|
this._clientSocketHandler = clientSocketHandler
|
||||||
|
this._connectedSockets = 0
|
||||||
|
|
||||||
function onRequest (req, res) {
|
this._http = http.createServer(this.handleHttpRequest)
|
||||||
logger.log('info', '%s requested: %s', util.getRemoteAddr(req), req.url)
|
|
||||||
distHandler(req, res, function () {
|
this._distServeStatic = serveStatic('dist/')
|
||||||
faviconsHandler(req, res, finalHandler(req, res))
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.start = function () {
|
module.exports = Server
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
279
lib/servers.js
Normal file
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
|
174
lib/util.js
174
lib/util.js
@ -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
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",
|
"name": "minetrack",
|
||||||
"version": "5.0.0",
|
"version": "5.1.0",
|
||||||
"description": "A Minecraft server tracker that lets you focus on the basics.",
|
"description": "A Minecraft server tracker that lets you focus on the basics.",
|
||||||
"main": "app.js",
|
"main": "main.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"finalhandler": "^1.1.2",
|
"finalhandler": "^1.1.2",
|
||||||
"mc-ping-updated": "0.1.1",
|
"mc-ping-updated": "0.1.1",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
while true;
|
while true;
|
||||||
do
|
do
|
||||||
node app.js
|
node main.js
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
done
|
Loading…
Reference in New Issue
Block a user