prettyify code

This commit is contained in:
Lee
2023-12-30 23:03:54 +00:00
parent 6fd5fdb7fe
commit ea15b979d5
28 changed files with 2179 additions and 1688 deletions

@ -1,87 +1,99 @@
const Database = require('./database')
const PingController = require('./ping')
const Server = require('./server')
const { TimeTracker } = require('./time')
const MessageOf = require('./message')
const Database = require("./database");
const PingController = require("./ping");
const Server = require("./server");
const { TimeTracker } = require("./time");
const MessageOf = require("./message");
const config = require('../config')
const minecraftVersions = require('../minecraft_versions')
const config = require("../config");
const minecraftVersions = require("../minecraft_versions");
class App {
serverRegistrations = []
serverRegistrations = [];
constructor () {
this.pingController = new PingController(this)
this.server = new Server(this)
this.timeTracker = new TimeTracker(this)
constructor() {
this.pingController = new PingController(this);
this.server = new Server(this);
this.timeTracker = new TimeTracker(this);
}
loadDatabase (callback) {
this.database = new Database(this)
loadDatabase(callback) {
this.database = new Database(this);
// Setup database instance
this.database.ensureIndexes(() => {
this.database.loadGraphPoints(config.graphDuration, () => {
this.database.loadRecords(() => {
if (config.oldPingsCleanup && config.oldPingsCleanup.enabled) {
this.database.initOldPingsDelete(callback)
this.database.initOldPingsDelete(callback);
} else {
callback()
callback();
}
})
})
})
});
});
});
}
handleReady () {
this.server.listen(config.site.ip, config.site.port)
handleReady() {
this.server.listen(config.site.ip, config.site.port);
// Allow individual modules to manage their own task scheduling
this.pingController.schedule()
this.pingController.schedule();
}
handleClientConnection = (client) => {
if (config.logToDatabase) {
client.on('message', (message) => {
if (message === 'requestHistoryGraph') {
client.on("message", (message) => {
if (message === "requestHistoryGraph") {
// Send historical graphData built from all serverRegistrations
const graphData = this.serverRegistrations.map(serverRegistration => serverRegistration.graphData)
const graphData = this.serverRegistrations.map(
(serverRegistration) => serverRegistration.graphData
);
// Send graphData in object wrapper to avoid needing to explicity filter
// any header data being appended by #MessageOf since the graph data is fed
// directly into the graphing system
client.send(MessageOf('historyGraph', {
timestamps: this.timeTracker.getGraphPoints(),
graphData
}))
client.send(
MessageOf("historyGraph", {
timestamps: this.timeTracker.getGraphPoints(),
graphData,
})
);
}
})
});
}
const initMessage = {
config: (() => {
// Remap minecraftVersion entries into name values
const minecraftVersionNames = {}
const minecraftVersionNames = {};
Object.keys(minecraftVersions).forEach(function (key) {
minecraftVersionNames[key] = minecraftVersions[key].map(version => version.name)
})
minecraftVersionNames[key] = minecraftVersions[key].map(
(version) => version.name
);
});
// Send configuration data for rendering the page
return {
graphDurationLabel: config.graphDurationLabel || (Math.floor(config.graphDuration / (60 * 60 * 1000)) + 'h'),
graphDurationLabel:
config.graphDurationLabel ||
Math.floor(config.graphDuration / (60 * 60 * 1000)) + "h",
graphMaxLength: TimeTracker.getMaxGraphDataLength(),
serverGraphMaxLength: TimeTracker.getMaxServerGraphDataLength(),
servers: this.serverRegistrations.map(serverRegistration => serverRegistration.getPublicData()),
servers: this.serverRegistrations.map((serverRegistration) =>
serverRegistration.getPublicData()
),
minecraftVersions: minecraftVersionNames,
isGraphVisible: config.logToDatabase
}
isGraphVisible: config.logToDatabase,
};
})(),
timestampPoints: this.timeTracker.getServerGraphPoints(),
servers: this.serverRegistrations.map(serverRegistration => serverRegistration.getPingHistory())
}
servers: this.serverRegistrations.map((serverRegistration) =>
serverRegistration.getPingHistory()
),
};
client.send(MessageOf('init', initMessage))
}
client.send(MessageOf("init", initMessage));
};
}
module.exports = App
module.exports = App;

@ -1,308 +1,364 @@
const sqlite = require('sqlite3')
const sqlite = require("sqlite3");
const logger = require('./logger')
const logger = require("./logger");
const config = require('../config')
const { TimeTracker } = require('./time')
const dataFolder = 'data/';
const config = require("../config");
const { TimeTracker } = require("./time");
const dataFolder = "data/";
class Database {
constructor (app) {
this._app = app
this._sql = new sqlite.Database(dataFolder + 'database.sql')
constructor(app) {
this._app = app;
this._sql = new sqlite.Database(dataFolder + "database.sql");
}
getDailyDatabase () {
getDailyDatabase() {
if (!config.createDailyDatabaseCopy) {
return
return;
}
const date = new Date()
const fileName = `database_copy_${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}.sql`
const date = new Date();
const fileName = `database_copy_${date.getDate()}-${
date.getMonth() + 1
}-${date.getFullYear()}.sql`;
if (fileName !== this._currentDatabaseCopyFileName) {
if (this._currentDatabaseCopyInstance) {
this._currentDatabaseCopyInstance.close()
this._currentDatabaseCopyInstance.close();
}
this._currentDatabaseCopyInstance = new sqlite.Database(dataFolder + fileName)
this._currentDatabaseCopyFileName = fileName
this._currentDatabaseCopyInstance = new sqlite.Database(
dataFolder + fileName
);
this._currentDatabaseCopyFileName = fileName;
// Ensure the initial tables are created
// This does not created indexes since it is only inserted to
this._currentDatabaseCopyInstance.serialize(() => {
this._currentDatabaseCopyInstance.run('CREATE TABLE IF NOT EXISTS pings (timestamp BIGINT NOT NULL, ip TINYTEXT, playerCount MEDIUMINT)', err => {
if (err) {
logger.log('error', 'Cannot create initial table for daily database')
throw err
this._currentDatabaseCopyInstance.run(
"CREATE TABLE IF NOT EXISTS pings (timestamp BIGINT NOT NULL, ip TINYTEXT, playerCount MEDIUMINT)",
(err) => {
if (err) {
logger.log(
"error",
"Cannot create initial table for daily database"
);
throw err;
}
}
})
})
);
});
}
return this._currentDatabaseCopyInstance
return this._currentDatabaseCopyInstance;
}
ensureIndexes (callback) {
const handleError = err => {
ensureIndexes(callback) {
const handleError = (err) => {
if (err) {
logger.log('error', 'Cannot create table or table index')
throw err
logger.log("error", "Cannot create table or table index");
throw err;
}
}
};
this._sql.serialize(() => {
this._sql.run('CREATE TABLE IF NOT EXISTS pings (timestamp BIGINT NOT NULL, ip TINYTEXT, playerCount MEDIUMINT)', handleError)
this._sql.run('CREATE TABLE IF NOT EXISTS players_record (timestamp BIGINT, ip TINYTEXT NOT NULL PRIMARY KEY, playerCount MEDIUMINT)', handleError)
this._sql.run('CREATE INDEX IF NOT EXISTS ip_index ON pings (ip, playerCount)', handleError)
this._sql.run('CREATE INDEX IF NOT EXISTS timestamp_index on PINGS (timestamp)', [], err => {
handleError(err)
// Queries are executed one at a time; this is the last one.
// Note that queries not scheduled directly in the callback function of
// #serialize are not necessarily serialized.
callback()
})
})
this._sql.run(
"CREATE TABLE IF NOT EXISTS pings (timestamp BIGINT NOT NULL, ip TINYTEXT, playerCount MEDIUMINT)",
handleError
);
this._sql.run(
"CREATE TABLE IF NOT EXISTS players_record (timestamp BIGINT, ip TINYTEXT NOT NULL PRIMARY KEY, playerCount MEDIUMINT)",
handleError
);
this._sql.run(
"CREATE INDEX IF NOT EXISTS ip_index ON pings (ip, playerCount)",
handleError
);
this._sql.run(
"CREATE INDEX IF NOT EXISTS timestamp_index on PINGS (timestamp)",
[],
(err) => {
handleError(err);
// Queries are executed one at a time; this is the last one.
// Note that queries not scheduled directly in the callback function of
// #serialize are not necessarily serialized.
callback();
}
);
});
}
loadGraphPoints (graphDuration, callback) {
loadGraphPoints(graphDuration, callback) {
// Query recent pings
const endTime = TimeTracker.getEpochMillis()
const startTime = endTime - graphDuration
const endTime = TimeTracker.getEpochMillis();
const startTime = endTime - graphDuration;
this.getRecentPings(startTime, endTime, pingData => {
const relativeGraphData = []
this.getRecentPings(startTime, endTime, (pingData) => {
const relativeGraphData = [];
for (const row of pingData) {
// Load into temporary array
// This will be culled prior to being pushed to the serverRegistration
let graphData = relativeGraphData[row.ip]
let graphData = relativeGraphData[row.ip];
if (!graphData) {
relativeGraphData[row.ip] = graphData = [[], []]
relativeGraphData[row.ip] = graphData = [[], []];
}
// DANGER!
// This will pull the timestamp from each row into memory
// This is built under the assumption that each round of pings shares the same timestamp
// This enables all timestamp arrays to have consistent point selection and graph correctly
graphData[0].push(row.timestamp)
graphData[1].push(row.playerCount)
graphData[0].push(row.timestamp);
graphData[1].push(row.playerCount);
}
Object.keys(relativeGraphData).forEach(ip => {
Object.keys(relativeGraphData).forEach((ip) => {
// Match IPs to serverRegistration object
for (const serverRegistration of this._app.serverRegistrations) {
if (serverRegistration.data.ip === ip) {
const graphData = relativeGraphData[ip]
const graphData = relativeGraphData[ip];
// Push the data into the instance and cull if needed
serverRegistration.loadGraphPoints(startTime, graphData[0], graphData[1])
serverRegistration.loadGraphPoints(
startTime,
graphData[0],
graphData[1]
);
break
break;
}
}
})
});
// Since all timestamps are shared, use the array from the first ServerRegistration
// This is very dangerous and can break if data is out of sync
if (Object.keys(relativeGraphData).length > 0) {
const serverIp = Object.keys(relativeGraphData)[0]
const timestamps = relativeGraphData[serverIp][0]
const serverIp = Object.keys(relativeGraphData)[0];
const timestamps = relativeGraphData[serverIp][0];
this._app.timeTracker.loadGraphPoints(startTime, timestamps)
this._app.timeTracker.loadGraphPoints(startTime, timestamps);
}
callback()
})
callback();
});
}
loadRecords (callback) {
let completedTasks = 0
loadRecords(callback) {
let completedTasks = 0;
this._app.serverRegistrations.forEach(serverRegistration => {
this._app.serverRegistrations.forEach((serverRegistration) => {
// Find graphPeaks
// This pre-computes the values prior to clients connecting
serverRegistration.findNewGraphPeak()
serverRegistration.findNewGraphPeak();
// Query recordData
// When complete increment completeTasks to know when complete
this.getRecord(serverRegistration.data.ip, (hasRecord, playerCount, timestamp) => {
if (hasRecord) {
serverRegistration.recordData = {
playerCount,
timestamp: TimeTracker.toSeconds(timestamp)
}
} else {
this.getRecordLegacy(serverRegistration.data.ip, (hasRecordLegacy, playerCountLegacy, timestampLegacy) => {
// New values that will be inserted to table
let newTimestamp = null
let newPlayerCount = null
// If legacy record found, use it for insertion
if (hasRecordLegacy) {
newTimestamp = timestampLegacy
newPlayerCount = playerCountLegacy
}
// Set record to recordData
this.getRecord(
serverRegistration.data.ip,
(hasRecord, playerCount, timestamp) => {
if (hasRecord) {
serverRegistration.recordData = {
playerCount: newPlayerCount,
timestamp: TimeTracker.toSeconds(newTimestamp)
}
playerCount,
timestamp: TimeTracker.toSeconds(timestamp),
};
} else {
this.getRecordLegacy(
serverRegistration.data.ip,
(hasRecordLegacy, playerCountLegacy, timestampLegacy) => {
// New values that will be inserted to table
let newTimestamp = null;
let newPlayerCount = null;
// Insert server entry to records table
const statement = this._sql.prepare('INSERT INTO players_record (timestamp, ip, playerCount) VALUES (?, ?, ?)')
statement.run(newTimestamp, serverRegistration.data.ip, newPlayerCount, err => {
if (err) {
logger.error(`Cannot insert initial player count record of ${serverRegistration.data.ip}`)
throw err
// If legacy record found, use it for insertion
if (hasRecordLegacy) {
newTimestamp = timestampLegacy;
newPlayerCount = playerCountLegacy;
}
// Set record to recordData
serverRegistration.recordData = {
playerCount: newPlayerCount,
timestamp: TimeTracker.toSeconds(newTimestamp),
};
// Insert server entry to records table
const statement = this._sql.prepare(
"INSERT INTO players_record (timestamp, ip, playerCount) VALUES (?, ?, ?)"
);
statement.run(
newTimestamp,
serverRegistration.data.ip,
newPlayerCount,
(err) => {
if (err) {
logger.error(
`Cannot insert initial player count record of ${serverRegistration.data.ip}`
);
throw err;
}
}
);
statement.finalize();
}
})
statement.finalize()
})
}
);
}
// Check if completedTasks hit the finish value
// Fire callback since #readyDatabase is complete
if (++completedTasks === this._app.serverRegistrations.length) {
callback()
// 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
], (err, data) => {
if (err) {
logger.log('error', 'Cannot get recent pings')
throw err
getRecentPings(startTime, endTime, callback) {
this._sql.all(
"SELECT * FROM pings WHERE timestamp >= ? AND timestamp <= ?",
[startTime, endTime],
(err, data) => {
if (err) {
logger.log("error", "Cannot get recent pings");
throw err;
}
callback(data);
}
callback(data)
})
);
}
getRecord (ip, callback) {
this._sql.all('SELECT playerCount, timestamp FROM players_record WHERE ip = ?', [
ip
], (err, data) => {
if (err) {
logger.log('error', `Cannot get ping record for ${ip}`)
throw err
}
getRecord(ip, callback) {
this._sql.all(
"SELECT playerCount, timestamp FROM players_record WHERE ip = ?",
[ip],
(err, data) => {
if (err) {
logger.log("error", `Cannot get ping record for ${ip}`);
throw err;
}
// Record not found
if (data[0] === undefined) {
// Record not found
if (data[0] === undefined) {
// eslint-disable-next-line node/no-callback-literal
callback(false);
return;
}
const playerCount = data[0].playerCount;
const timestamp = data[0].timestamp;
// Allow null player counts and timestamps, the frontend will safely handle them
// eslint-disable-next-line node/no-callback-literal
callback(false)
return
callback(true, playerCount, timestamp);
}
const playerCount = data[0].playerCount
const timestamp = data[0].timestamp
// Allow null player counts and timestamps, the frontend will safely handle them
// eslint-disable-next-line node/no-callback-literal
callback(true, playerCount, timestamp)
})
);
}
// Retrieves record from pings table, used for converting to separate table
getRecordLegacy (ip, callback) {
this._sql.all('SELECT MAX(playerCount), timestamp FROM pings WHERE ip = ?', [
ip
], (err, data) => {
if (err) {
logger.log('error', `Cannot get legacy ping record for ${ip}`)
throw err
}
getRecordLegacy(ip, callback) {
this._sql.all(
"SELECT MAX(playerCount), timestamp FROM pings WHERE ip = ?",
[ip],
(err, data) => {
if (err) {
logger.log("error", `Cannot get legacy ping record for ${ip}`);
throw err;
}
// For empty results, data will be length 1 with [null, null]
const playerCount = data[0]['MAX(playerCount)']
const timestamp = data[0].timestamp
// For empty results, data will be length 1 with [null, null]
const playerCount = data[0]["MAX(playerCount)"];
const timestamp = data[0].timestamp;
// Allow null timestamps, the frontend will safely handle them
// This allows insertion of free standing records without a known timestamp
if (playerCount !== null) {
// eslint-disable-next-line node/no-callback-literal
callback(true, playerCount, timestamp)
} else {
// eslint-disable-next-line node/no-callback-literal
callback(false)
// Allow null timestamps, the frontend will safely handle them
// This allows insertion of free standing records without a known timestamp
if (playerCount !== null) {
// eslint-disable-next-line node/no-callback-literal
callback(true, playerCount, timestamp);
} else {
// eslint-disable-next-line node/no-callback-literal
callback(false);
}
}
})
);
}
insertPing (ip, timestamp, unsafePlayerCount) {
this._insertPingTo(ip, timestamp, unsafePlayerCount, this._sql)
insertPing(ip, timestamp, unsafePlayerCount) {
this._insertPingTo(ip, timestamp, unsafePlayerCount, this._sql);
// Push a copy of the data into the database copy, if any
// This creates an "insert only" copy of the database for archiving
const dailyDatabase = this.getDailyDatabase()
const dailyDatabase = this.getDailyDatabase();
if (dailyDatabase) {
this._insertPingTo(ip, timestamp, unsafePlayerCount, dailyDatabase)
this._insertPingTo(ip, timestamp, unsafePlayerCount, dailyDatabase);
}
}
_insertPingTo (ip, timestamp, unsafePlayerCount, db) {
const statement = db.prepare('INSERT INTO pings (timestamp, ip, playerCount) VALUES (?, ?, ?)')
statement.run(timestamp, ip, unsafePlayerCount, err => {
_insertPingTo(ip, timestamp, unsafePlayerCount, db) {
const statement = db.prepare(
"INSERT INTO pings (timestamp, ip, playerCount) VALUES (?, ?, ?)"
);
statement.run(timestamp, ip, unsafePlayerCount, (err) => {
if (err) {
logger.error(`Cannot insert ping record of ${ip} at ${timestamp}`)
throw err
logger.error(`Cannot insert ping record of ${ip} at ${timestamp}`);
throw err;
}
})
statement.finalize()
});
statement.finalize();
}
updatePlayerCountRecord (ip, playerCount, timestamp) {
const statement = this._sql.prepare('UPDATE players_record SET timestamp = ?, playerCount = ? WHERE ip = ?')
statement.run(timestamp, playerCount, ip, err => {
updatePlayerCountRecord(ip, playerCount, timestamp) {
const statement = this._sql.prepare(
"UPDATE players_record SET timestamp = ?, playerCount = ? WHERE ip = ?"
);
statement.run(timestamp, playerCount, ip, (err) => {
if (err) {
logger.error(`Cannot update player count record of ${ip} at ${timestamp}`)
throw err
logger.error(
`Cannot update player count record of ${ip} at ${timestamp}`
);
throw err;
}
})
statement.finalize()
});
statement.finalize();
}
initOldPingsDelete (callback) {
initOldPingsDelete(callback) {
// Delete old pings on startup
logger.info('Deleting old pings..')
logger.info("Deleting old pings..");
this.deleteOldPings(() => {
const oldPingsCleanupInterval = config.oldPingsCleanup.interval || 3600000
const oldPingsCleanupInterval =
config.oldPingsCleanup.interval || 3600000;
if (oldPingsCleanupInterval > 0) {
// Delete old pings periodically
setInterval(() => this.deleteOldPings(), oldPingsCleanupInterval)
setInterval(() => this.deleteOldPings(), oldPingsCleanupInterval);
}
callback()
})
callback();
});
}
deleteOldPings (callback) {
deleteOldPings(callback) {
// The oldest timestamp that will be kept
const oldestTimestamp = TimeTracker.getEpochMillis() - config.graphDuration
const oldestTimestamp = TimeTracker.getEpochMillis() - config.graphDuration;
const deleteStart = TimeTracker.getEpochMillis()
const statement = this._sql.prepare('DELETE FROM pings WHERE timestamp < ?;')
statement.run(oldestTimestamp, err => {
const deleteStart = TimeTracker.getEpochMillis();
const statement = this._sql.prepare(
"DELETE FROM pings WHERE timestamp < ?;"
);
statement.run(oldestTimestamp, (err) => {
if (err) {
logger.error('Cannot delete old pings')
throw err
logger.error("Cannot delete old pings");
throw err;
} else {
const deleteTook = TimeTracker.getEpochMillis() - deleteStart
logger.info(`Old pings deleted in ${deleteTook}ms`)
const deleteTook = TimeTracker.getEpochMillis() - deleteStart;
logger.info(`Old pings deleted in ${deleteTook}ms`);
if (callback) {
callback()
callback();
}
}
})
statement.finalize()
});
statement.finalize();
}
}
module.exports = Database
module.exports = Database;

@ -1,78 +1,97 @@
const dns = require('dns')
const dns = require("dns");
const logger = require('./logger')
const logger = require("./logger");
const { TimeTracker } = require('./time')
const { TimeTracker } = require("./time");
const config = require('../config')
const config = require("../config");
const SKIP_SRV_TIMEOUT = config.skipSrvTimeout || 60 * 60 * 1000
const SKIP_SRV_TIMEOUT = config.skipSrvTimeout || 60 * 60 * 1000;
class DNSResolver {
constructor (ip, port) {
this._ip = ip
this._port = port
constructor(ip, port) {
this._ip = ip;
this._port = port;
}
_skipSrv () {
this._skipSrvUntil = TimeTracker.getEpochMillis() + SKIP_SRV_TIMEOUT
_skipSrv() {
this._skipSrvUntil = TimeTracker.getEpochMillis() + SKIP_SRV_TIMEOUT;
}
_isSkipSrv () {
return this._skipSrvUntil && TimeTracker.getEpochMillis() <= this._skipSrvUntil
_isSkipSrv() {
return (
this._skipSrvUntil && TimeTracker.getEpochMillis() <= this._skipSrvUntil
);
}
resolve (callback) {
resolve(callback) {
if (this._isSkipSrv()) {
callback(this._ip, this._port, config.rates.connectTimeout)
callback(this._ip, this._port, config.rates.connectTimeout);
return
return;
}
const startTime = TimeTracker.getEpochMillis()
const startTime = TimeTracker.getEpochMillis();
let callbackFired = false
let callbackFired = false;
const fireCallback = (ip, port) => {
if (!callbackFired) {
callbackFired = true
callbackFired = true;
// Send currentTime - startTime to provide remaining connectionTime allowance
const remainingTime = config.rates.connectTimeout - (TimeTracker.getEpochMillis() - startTime)
const remainingTime =
config.rates.connectTimeout -
(TimeTracker.getEpochMillis() - startTime);
callback(ip || this._ip, port || this._port, remainingTime)
callback(ip || this._ip, port || this._port, remainingTime);
}
}
};
const timeoutCallback = setTimeout(fireCallback, config.rates.connectTimeout)
const timeoutCallback = setTimeout(
fireCallback,
config.rates.connectTimeout
);
dns.resolveSrv('_minecraft._tcp.' + this._ip, (err, records) => {
dns.resolveSrv("_minecraft._tcp." + this._ip, (err, records) => {
// Cancel the timeout handler if not already fired
if (!callbackFired) {
clearTimeout(timeoutCallback)
clearTimeout(timeoutCallback);
}
// Test if the error indicates a miss, or if the records returned are empty
if ((err && (err.code === 'ENOTFOUND' || err.code === 'ENODATA')) || !records || records.length === 0) {
if (
(err && (err.code === "ENOTFOUND" || err.code === "ENODATA")) ||
!records ||
records.length === 0
) {
// Compare config.skipSrvTimeout directly since SKIP_SRV_TIMEOUT has an or'd value
// isSkipSrvTimeoutDisabled == whether the config has a valid skipSrvTimeout value set
const isSkipSrvTimeoutDisabled = typeof config.skipSrvTimeout === 'number' && config.skipSrvTimeout === 0
const isSkipSrvTimeoutDisabled =
typeof config.skipSrvTimeout === "number" &&
config.skipSrvTimeout === 0;
// Only activate _skipSrv if the skipSrvTimeout value is either NaN or > 0
// 0 represents a disabled flag
if (!this._isSkipSrv() && !isSkipSrvTimeoutDisabled) {
this._skipSrv()
this._skipSrv();
logger.log('warn', 'No SRV records were resolved for %s. Minetrack will skip attempting to resolve %s SRV records for %d minutes.', this._ip, this._ip, SKIP_SRV_TIMEOUT / (60 * 1000))
logger.log(
"warn",
"No SRV records were resolved for %s. Minetrack will skip attempting to resolve %s SRV records for %d minutes.",
this._ip,
this._ip,
SKIP_SRV_TIMEOUT / (60 * 1000)
);
}
fireCallback()
fireCallback();
} else {
// Only fires if !err && records.length > 0
fireCallback(records[0].name, records[0].port)
fireCallback(records[0].name, records[0].port);
}
})
});
}
}
module.exports = DNSResolver
module.exports = DNSResolver;

@ -1,17 +1,17 @@
const 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: () => {
const date = new Date()
return date.toLocaleTimeString() + ' ' + date.toLocaleDateString()
const date = new Date();
return date.toLocaleTimeString() + " " + date.toLocaleDateString();
},
colorize: true
})
colorize: true,
});
module.exports = winston
module.exports = winston;

@ -1,6 +1,6 @@
module.exports = function MessageOf (name, data) {
module.exports = function MessageOf(name, data) {
return JSON.stringify({
message: name,
...data
})
}
...data,
});
};

@ -1,159 +1,212 @@
const minecraftJavaPing = require('mcping-js')
const minecraftBedrockPing = require('mcpe-ping-fixed')
const minecraftJavaPing = require("mcping-js");
const minecraftBedrockPing = require("mcpe-ping-fixed");
const logger = require('./logger')
const MessageOf = require('./message')
const { TimeTracker } = require('./time')
const logger = require("./logger");
const MessageOf = require("./message");
const { TimeTracker } = require("./time");
const { getPlayerCountOrNull } = require('./util')
const { getPlayerCountOrNull } = require("./util");
const config = require('../config')
const config = require("../config");
function ping (serverRegistration, timeout, callback, version) {
function ping(serverRegistration, timeout, callback, version) {
switch (serverRegistration.data.type) {
case 'PC':
case "PC":
serverRegistration.dnsResolver.resolve((host, port, remainingTimeout) => {
const server = new minecraftJavaPing.MinecraftServer(host, port || 25565)
const server = new minecraftJavaPing.MinecraftServer(
host,
port || 25565
);
server.ping(remainingTimeout, version, (err, res) => {
if (err) {
callback(err)
callback(err);
} else {
const payload = {
players: {
online: capPlayerCount(serverRegistration.data.ip, parseInt(res.players.online))
online: capPlayerCount(
serverRegistration.data.ip,
parseInt(res.players.online)
),
},
version: parseInt(res.version.protocol)
}
version: parseInt(res.version.protocol),
};
// Ensure the returned favicon is a data URI
if (res.favicon && res.favicon.startsWith('data:image/')) {
payload.favicon = res.favicon
if (res.favicon && res.favicon.startsWith("data:image/")) {
payload.favicon = res.favicon;
}
callback(null, payload)
callback(null, payload);
}
})
})
break
});
});
break;
case 'PE':
minecraftBedrockPing(serverRegistration.data.ip, serverRegistration.data.port || 19132, (err, res) => {
if (err) {
callback(err)
} else {
callback(null, {
players: {
online: capPlayerCount(serverRegistration.data.ip, parseInt(res.currentPlayers))
}
})
}
}, timeout)
break
case "PE":
minecraftBedrockPing(
serverRegistration.data.ip,
serverRegistration.data.port || 19132,
(err, res) => {
if (err) {
callback(err);
} else {
callback(null, {
players: {
online: capPlayerCount(
serverRegistration.data.ip,
parseInt(res.currentPlayers)
),
},
});
}
},
timeout
);
break;
default:
throw new Error('Unsupported type: ' + serverRegistration.data.type)
throw new Error("Unsupported type: " + serverRegistration.data.type);
}
}
// 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
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)
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)) {
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;
}
class PingController {
constructor (app) {
this._app = app
this._isRunningTasks = false
constructor(app) {
this._app = app;
this._isRunningTasks = false;
}
schedule () {
setInterval(this.pingAll, config.rates.pingAll)
schedule() {
setInterval(this.pingAll, config.rates.pingAll);
this.pingAll()
this.pingAll();
}
pingAll = () => {
const { timestamp, updateHistoryGraph } = this._app.timeTracker.newPointTimestamp()
const { timestamp, updateHistoryGraph } =
this._app.timeTracker.newPointTimestamp();
this.startPingTasks(results => {
const updates = []
this.startPingTasks((results) => {
const updates = [];
for (const serverRegistration of this._app.serverRegistrations) {
const result = results[serverRegistration.serverId]
const result = results[serverRegistration.serverId];
// Log to database if enabled
// Use null to represent a failed ping
if (config.logToDatabase) {
const unsafePlayerCount = getPlayerCountOrNull(result.resp)
const unsafePlayerCount = getPlayerCountOrNull(result.resp);
this._app.database.insertPing(serverRegistration.data.ip, timestamp, unsafePlayerCount)
this._app.database.insertPing(
serverRegistration.data.ip,
timestamp,
unsafePlayerCount
);
}
// Generate a combined update payload
// This includes any modified fields and flags used by the frontend
// This will not be cached and can contain live metadata
const update = serverRegistration.handlePing(timestamp, result.resp, result.err, result.version, updateHistoryGraph)
const update = serverRegistration.handlePing(
timestamp,
result.resp,
result.err,
result.version,
updateHistoryGraph
);
updates[serverRegistration.serverId] = update
updates[serverRegistration.serverId] = update;
}
// Send object since updates uses serverIds as keys
// Send a single timestamp entry since it is shared
this._app.server.broadcast(MessageOf('updateServers', {
timestamp: TimeTracker.toSeconds(timestamp),
updateHistoryGraph,
updates
}))
})
}
this._app.server.broadcast(
MessageOf("updateServers", {
timestamp: TimeTracker.toSeconds(timestamp),
updateHistoryGraph,
updates,
})
);
});
};
startPingTasks = (callback) => {
if (this._isRunningTasks) {
logger.log('warn', 'Started re-pinging servers before the last loop has finished! You may need to increase "rates.pingAll" in config.json')
logger.log(
"warn",
'Started re-pinging servers before the last loop has finished! You may need to increase "rates.pingAll" in config.json'
);
return
return;
}
this._isRunningTasks = true
this._isRunningTasks = true;
const results = []
const results = [];
for (const serverRegistration of this._app.serverRegistrations) {
const version = serverRegistration.getNextProtocolVersion()
const version = serverRegistration.getNextProtocolVersion();
ping(serverRegistration, config.rates.connectTimeout, (err, resp) => {
if (err && config.logFailedPings !== false) {
logger.log('error', 'Failed to ping %s: %s', serverRegistration.data.ip, err.message)
}
ping(
serverRegistration,
config.rates.connectTimeout,
(err, resp) => {
if (err && config.logFailedPings !== false) {
logger.log(
"error",
"Failed to ping %s: %s",
serverRegistration.data.ip,
err.message
);
}
results[serverRegistration.serverId] = {
resp,
err,
version
}
results[serverRegistration.serverId] = {
resp,
err,
version,
};
if (Object.keys(results).length === this._app.serverRegistrations.length) {
// Loop has completed, release the locking flag
this._isRunningTasks = false
if (
Object.keys(results).length === this._app.serverRegistrations.length
) {
// Loop has completed, release the locking flag
this._isRunningTasks = false;
callback(results)
}
}, version.protocolId)
callback(results);
}
},
version.protocolId
);
}
}
};
}
module.exports = PingController
module.exports = PingController;

@ -1,114 +1,139 @@
const http = require('http')
const format = require('util').format
const http = require("http");
const format = require("util").format;
const WebSocket = require('ws')
const finalHttpHandler = require('finalhandler')
const serveStatic = require('serve-static')
const WebSocket = require("ws");
const finalHttpHandler = require("finalhandler");
const serveStatic = require("serve-static");
const logger = require('./logger')
const logger = require("./logger");
const HASHED_FAVICON_URL_REGEX = /hashedfavicon_([a-z0-9]{32}).png/g
const HASHED_FAVICON_URL_REGEX = /hashedfavicon_([a-z0-9]{32}).png/g;
function getRemoteAddr (req) {
return req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddress
function getRemoteAddr(req) {
return (
req.headers["cf-connecting-ip"] ||
req.headers["x-forwarded-for"] ||
req.connection.remoteAddress
);
}
class Server {
static getHashedFaviconUrl (hash) {
static getHashedFaviconUrl(hash) {
// Format must be compatible with HASHED_FAVICON_URL_REGEX
return format('/hashedfavicon_%s.png', hash)
return format("/hashedfavicon_%s.png", hash);
}
constructor (app) {
this._app = app
constructor(app) {
this._app = app;
this.createHttpServer()
this.createWebSocketServer()
this.createHttpServer();
this.createWebSocketServer();
}
createHttpServer () {
const distServeStatic = serveStatic('dist/')
const faviconsServeStatic = serveStatic('favicons/')
createHttpServer() {
const distServeStatic = serveStatic("dist/");
const faviconsServeStatic = serveStatic("favicons/");
this._http = http.createServer((req, res) => {
logger.log('info', '%s requested: %s', getRemoteAddr(req), req.url)
logger.log("info", "%s requested: %s", getRemoteAddr(req), req.url);
// Test the URL against a regex for hashed favicon URLs
// Require only 1 match ([0]) and test its first captured group ([1])
// Any invalid value or hit miss will pass into static handlers below
const faviconHash = [...req.url.matchAll(HASHED_FAVICON_URL_REGEX)]
const faviconHash = [...req.url.matchAll(HASHED_FAVICON_URL_REGEX)];
if (faviconHash.length === 1 && this.handleFaviconRequest(res, faviconHash[0][1])) {
return
if (
faviconHash.length === 1 &&
this.handleFaviconRequest(res, faviconHash[0][1])
) {
return;
}
// Attempt to handle req using distServeStatic, otherwise fail over to faviconServeStatic
// If faviconServeStatic fails, pass to finalHttpHandler to terminate
distServeStatic(req, res, () => {
faviconsServeStatic(req, res, finalHttpHandler(req, res))
})
})
faviconsServeStatic(req, res, finalHttpHandler(req, res));
});
});
}
handleFaviconRequest = (res, faviconHash) => {
for (const serverRegistration of this._app.serverRegistrations) {
if (serverRegistration.faviconHash && serverRegistration.faviconHash === faviconHash) {
const buf = Buffer.from(serverRegistration.lastFavicon.split(',')[1], 'base64')
if (
serverRegistration.faviconHash &&
serverRegistration.faviconHash === faviconHash
) {
const buf = Buffer.from(
serverRegistration.lastFavicon.split(",")[1],
"base64"
);
res.writeHead(200, {
'Content-Type': 'image/png',
'Content-Length': buf.length,
'Cache-Control': 'public, max-age=604800' // Cache hashed favicon for 7 days
}).end(buf)
res
.writeHead(200, {
"Content-Type": "image/png",
"Content-Length": buf.length,
"Cache-Control": "public, max-age=604800", // Cache hashed favicon for 7 days
})
.end(buf);
return true
return true;
}
}
return false
}
return false;
};
createWebSocketServer () {
createWebSocketServer() {
this._wss = new WebSocket.Server({
server: this._http
})
server: this._http,
});
this._wss.on('connection', (client, req) => {
logger.log('info', '%s connected, total clients: %d', getRemoteAddr(req), this.getConnectedClients())
this._wss.on("connection", (client, req) => {
logger.log(
"info",
"%s connected, total clients: %d",
getRemoteAddr(req),
this.getConnectedClients()
);
// Bind disconnect event for logging
client.on('close', () => {
logger.log('info', '%s disconnected, total clients: %d', getRemoteAddr(req), this.getConnectedClients())
})
client.on("close", () => {
logger.log(
"info",
"%s disconnected, total clients: %d",
getRemoteAddr(req),
this.getConnectedClients()
);
});
// Pass client off to proxy handler
this._app.handleClientConnection(client)
})
this._app.handleClientConnection(client);
});
}
listen (host, port) {
this._http.listen(port, host)
listen(host, port) {
this._http.listen(port, host);
logger.log('info', 'Started on %s:%d', host, port)
logger.log("info", "Started on %s:%d", host, port);
}
broadcast (payload) {
this._wss.clients.forEach(client => {
broadcast(payload) {
this._wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(payload)
client.send(payload);
}
})
});
}
getConnectedClients () {
let count = 0
this._wss.clients.forEach(client => {
getConnectedClients() {
let count = 0;
this._wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
count++
count++;
}
})
return count
});
return count;
}
}
module.exports = Server
module.exports = Server;

@ -1,263 +1,297 @@
const crypto = require('crypto')
const crypto = require("crypto");
const DNSResolver = require('./dns')
const Server = require('./server')
const DNSResolver = require("./dns");
const Server = require("./server");
const { GRAPH_UPDATE_TIME_GAP, TimeTracker } = require('./time')
const { getPlayerCountOrNull } = require('./util')
const { GRAPH_UPDATE_TIME_GAP, TimeTracker } = require("./time");
const { getPlayerCountOrNull } = require("./util");
const config = require('../config')
const minecraftVersions = require('../minecraft_versions')
const config = require("../config");
const minecraftVersions = require("../minecraft_versions");
class ServerRegistration {
serverId
lastFavicon
versions = []
recordData
graphData = []
serverId;
lastFavicon;
versions = [];
recordData;
graphData = [];
constructor (app, serverId, data) {
this._app = app
this.serverId = serverId
this.data = data
this._pingHistory = []
this.dnsResolver = new DNSResolver(this.data.ip, this.data.port)
constructor(app, serverId, data) {
this._app = app;
this.serverId = serverId;
this.data = data;
this._pingHistory = [];
this.dnsResolver = new DNSResolver(this.data.ip, this.data.port);
}
handlePing (timestamp, resp, err, version, updateHistoryGraph) {
handlePing(timestamp, resp, err, version, updateHistoryGraph) {
// Use null to represent a failed ping
const unsafePlayerCount = getPlayerCountOrNull(resp)
const unsafePlayerCount = getPlayerCountOrNull(resp);
// Store into in-memory ping data
TimeTracker.pushAndShift(this._pingHistory, unsafePlayerCount, TimeTracker.getMaxServerGraphDataLength())
TimeTracker.pushAndShift(
this._pingHistory,
unsafePlayerCount,
TimeTracker.getMaxServerGraphDataLength()
);
// Only notify the frontend to append to the historical graph
// if both the graphing behavior is enabled and the backend agrees
// that the ping is eligible for addition
if (updateHistoryGraph) {
TimeTracker.pushAndShift(this.graphData, unsafePlayerCount, TimeTracker.getMaxGraphDataLength())
TimeTracker.pushAndShift(
this.graphData,
unsafePlayerCount,
TimeTracker.getMaxGraphDataLength()
);
}
// Delegate out update payload generation
return this.getUpdate(timestamp, resp, err, version)
return this.getUpdate(timestamp, resp, err, version);
}
getUpdate (timestamp, resp, err, version) {
const update = {}
getUpdate(timestamp, resp, err, version) {
const update = {};
// Always append a playerCount value
// When resp is undefined (due to an error), playerCount will be null
update.playerCount = getPlayerCountOrNull(resp)
update.playerCount = getPlayerCountOrNull(resp);
if (resp) {
if (resp.version && this.updateProtocolVersionCompat(resp.version, version.protocolId, version.protocolIndex)) {
if (
resp.version &&
this.updateProtocolVersionCompat(
resp.version,
version.protocolId,
version.protocolIndex
)
) {
// Append an updated version listing
update.versions = this.versions
update.versions = this.versions;
}
if (config.logToDatabase && (!this.recordData || resp.players.online > this.recordData.playerCount)) {
if (
config.logToDatabase &&
(!this.recordData || resp.players.online > this.recordData.playerCount)
) {
this.recordData = {
playerCount: resp.players.online,
timestamp: TimeTracker.toSeconds(timestamp)
}
timestamp: TimeTracker.toSeconds(timestamp),
};
// Append an updated recordData
update.recordData = this.recordData
update.recordData = this.recordData;
// Update record in database
this._app.database.updatePlayerCountRecord(this.data.ip, resp.players.online, timestamp)
this._app.database.updatePlayerCountRecord(
this.data.ip,
resp.players.online,
timestamp
);
}
if (this.updateFavicon(resp.favicon)) {
update.favicon = this.getFaviconUrl()
update.favicon = this.getFaviconUrl();
}
if (config.logToDatabase) {
// 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 (this.findNewGraphPeak()) {
update.graphPeakData = this.getGraphPeak()
update.graphPeakData = this.getGraphPeak();
}
}
} else if (err) {
// Append a filtered copy of err
// This ensures any unintended data is not leaked
update.error = this.filterError(err)
update.error = this.filterError(err);
}
return update
return update;
}
getPingHistory () {
getPingHistory() {
if (this._pingHistory.length > 0) {
const payload = {
versions: this.versions,
recordData: this.recordData,
favicon: this.getFaviconUrl()
}
favicon: this.getFaviconUrl(),
};
// Only append graphPeakData if defined
// The value is lazy computed and conditional that config->logToDatabase == true
const graphPeakData = this.getGraphPeak()
const graphPeakData = this.getGraphPeak();
if (graphPeakData) {
payload.graphPeakData = graphPeakData
payload.graphPeakData = graphPeakData;
}
// Assume the ping was a success and define result
// pingHistory does not keep error references, so its impossible to detect if this is an error
// It is also pointless to store that data since it will be short lived
payload.playerCount = this._pingHistory[this._pingHistory.length - 1]
payload.playerCount = this._pingHistory[this._pingHistory.length - 1];
// Send a copy of pingHistory
// Include the last value even though it is contained within payload
// The frontend will only push to its graphData from playerCountHistory
payload.playerCountHistory = this._pingHistory
payload.playerCountHistory = this._pingHistory;
return payload
return payload;
}
return {
error: {
message: 'Pinging...'
message: "Pinging...",
},
recordData: this.recordData,
graphPeakData: this.getGraphPeak(),
favicon: this.data.favicon
}
favicon: this.data.favicon,
};
}
loadGraphPoints (startTime, timestamps, points) {
this.graphData = TimeTracker.everyN(timestamps, startTime, GRAPH_UPDATE_TIME_GAP, (i) => points[i])
loadGraphPoints(startTime, timestamps, points) {
this.graphData = TimeTracker.everyN(
timestamps,
startTime,
GRAPH_UPDATE_TIME_GAP,
(i) => points[i]
);
}
findNewGraphPeak () {
let index = -1
findNewGraphPeak() {
let index = -1;
for (let i = 0; i < this.graphData.length; i++) {
const point = this.graphData[i]
const point = this.graphData[i];
if (point !== null && (index === -1 || point > this.graphData[index])) {
index = i
index = i;
}
}
if (index >= 0) {
const lastGraphPeakIndex = this._graphPeakIndex
this._graphPeakIndex = index
return index !== lastGraphPeakIndex
const lastGraphPeakIndex = this._graphPeakIndex;
this._graphPeakIndex = index;
return index !== lastGraphPeakIndex;
} else {
this._graphPeakIndex = undefined
return false
this._graphPeakIndex = undefined;
return false;
}
}
getGraphPeak () {
getGraphPeak() {
if (this._graphPeakIndex === undefined) {
return
return;
}
return {
playerCount: this.graphData[this._graphPeakIndex],
timestamp: this._app.timeTracker.getGraphPointAt(this._graphPeakIndex)
}
timestamp: this._app.timeTracker.getGraphPointAt(this._graphPeakIndex),
};
}
updateFavicon (favicon) {
updateFavicon(favicon) {
// If data.favicon is defined, then a favicon override is present
// Disregard the incoming favicon, regardless if it is different
if (this.data.favicon) {
return false
return false;
}
if (favicon && favicon !== this.lastFavicon) {
this.lastFavicon = favicon
this.lastFavicon = favicon;
// Generate an updated hash
// This is used by #getFaviconUrl
this.faviconHash = crypto.createHash('md5').update(favicon).digest('hex').toString()
this.faviconHash = crypto
.createHash("md5")
.update(favicon)
.digest("hex")
.toString();
return true
return true;
}
return false
return false;
}
getFaviconUrl () {
getFaviconUrl() {
if (this.faviconHash) {
return Server.getHashedFaviconUrl(this.faviconHash)
return Server.getHashedFaviconUrl(this.faviconHash);
} else if (this.data.favicon) {
return this.data.favicon
return this.data.favicon;
}
}
updateProtocolVersionCompat (incomingId, outgoingId, protocolIndex) {
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)
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)
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)
this.versions.sort((a, b) => a - b);
return true
return true;
} else if (!isSuccess && indexOf >= 0) {
this.versions.splice(indexOf, 1)
return true
this.versions.splice(indexOf, 1);
return true;
}
return false
return false;
}
getNextProtocolVersion () {
getNextProtocolVersion() {
// Minecraft Bedrock Edition does not have protocol versions
if (this.data.type === 'PE') {
if (this.data.type === "PE") {
return {
protocolId: 0,
protocolIndex: 0
}
protocolIndex: 0,
};
}
const protocolVersions = minecraftVersions[this.data.type]
if (typeof this._nextProtocolIndex === 'undefined' || this._nextProtocolIndex + 1 >= protocolVersions.length) {
this._nextProtocolIndex = 0
const protocolVersions = minecraftVersions[this.data.type];
if (
typeof this._nextProtocolIndex === "undefined" ||
this._nextProtocolIndex + 1 >= protocolVersions.length
) {
this._nextProtocolIndex = 0;
} else {
this._nextProtocolIndex++
this._nextProtocolIndex++;
}
return {
protocolId: protocolVersions[this._nextProtocolIndex].protocolId,
protocolIndex: this._nextProtocolIndex
}
protocolIndex: this._nextProtocolIndex,
};
}
filterError (err) {
let message = 'Unknown error'
filterError(err) {
let message = "Unknown error";
// Attempt to match to the first possible value
for (const key of ['message', 'description', 'errno']) {
for (const key of ["message", "description", "errno"]) {
if (err[key]) {
message = err[key]
break
message = err[key];
break;
}
}
// Trim the message if too long
if (message.length > 28) {
message = message.substring(0, 28) + '...'
message = message.substring(0, 28) + "...";
}
return {
message: message
}
message: message,
};
}
getPublicData () {
getPublicData() {
// Return a custom object instead of data directly to avoid data leakage
return {
name: this.data.name,
ip: this.data.ip,
type: this.data.type,
color: this.data.color
}
color: this.data.color,
};
}
}
module.exports = ServerRegistration
module.exports = ServerRegistration;

@ -1,96 +1,112 @@
const config = require('../config.json')
const config = require("../config.json");
const GRAPH_UPDATE_TIME_GAP = 60 * 1000 // 60 seconds
const GRAPH_UPDATE_TIME_GAP = 60 * 1000; // 60 seconds
class TimeTracker {
constructor (app) {
this._app = app
this._serverGraphPoints = []
this._graphPoints = []
constructor(app) {
this._app = app;
this._serverGraphPoints = [];
this._graphPoints = [];
}
newPointTimestamp () {
const timestamp = TimeTracker.getEpochMillis()
newPointTimestamp() {
const timestamp = TimeTracker.getEpochMillis();
TimeTracker.pushAndShift(this._serverGraphPoints, timestamp, TimeTracker.getMaxServerGraphDataLength())
TimeTracker.pushAndShift(
this._serverGraphPoints,
timestamp,
TimeTracker.getMaxServerGraphDataLength()
);
// Flag each group as history graph additions each minute
// This is sent to the frontend for graph updates
const updateHistoryGraph = config.logToDatabase && (!this._lastHistoryGraphUpdate || timestamp - this._lastHistoryGraphUpdate >= GRAPH_UPDATE_TIME_GAP)
const updateHistoryGraph =
config.logToDatabase &&
(!this._lastHistoryGraphUpdate ||
timestamp - this._lastHistoryGraphUpdate >= GRAPH_UPDATE_TIME_GAP);
if (updateHistoryGraph) {
this._lastHistoryGraphUpdate = timestamp
this._lastHistoryGraphUpdate = timestamp;
// Push into timestamps array to update backend state
TimeTracker.pushAndShift(this._graphPoints, timestamp, TimeTracker.getMaxGraphDataLength())
TimeTracker.pushAndShift(
this._graphPoints,
timestamp,
TimeTracker.getMaxGraphDataLength()
);
}
return {
timestamp,
updateHistoryGraph
}
updateHistoryGraph,
};
}
loadGraphPoints (startTime, timestamps) {
loadGraphPoints(startTime, timestamps) {
// This is a copy of ServerRegistration#loadGraphPoints
// timestamps contains original timestamp data and needs to be filtered into minutes
this._graphPoints = TimeTracker.everyN(timestamps, startTime, GRAPH_UPDATE_TIME_GAP, (i) => timestamps[i])
this._graphPoints = TimeTracker.everyN(
timestamps,
startTime,
GRAPH_UPDATE_TIME_GAP,
(i) => timestamps[i]
);
}
getGraphPointAt (i) {
return TimeTracker.toSeconds(this._graphPoints[i])
getGraphPointAt(i) {
return TimeTracker.toSeconds(this._graphPoints[i]);
}
getServerGraphPoints () {
return this._serverGraphPoints.map(TimeTracker.toSeconds)
getServerGraphPoints() {
return this._serverGraphPoints.map(TimeTracker.toSeconds);
}
getGraphPoints () {
return this._graphPoints.map(TimeTracker.toSeconds)
getGraphPoints() {
return this._graphPoints.map(TimeTracker.toSeconds);
}
static toSeconds = (timestamp) => {
return Math.floor(timestamp / 1000)
return Math.floor(timestamp / 1000);
};
static getEpochMillis() {
return new Date().getTime();
}
static getEpochMillis () {
return new Date().getTime()
static getMaxServerGraphDataLength() {
return Math.ceil(config.serverGraphDuration / config.rates.pingAll);
}
static getMaxServerGraphDataLength () {
return Math.ceil(config.serverGraphDuration / config.rates.pingAll)
static getMaxGraphDataLength() {
return Math.ceil(config.graphDuration / GRAPH_UPDATE_TIME_GAP);
}
static getMaxGraphDataLength () {
return Math.ceil(config.graphDuration / GRAPH_UPDATE_TIME_GAP)
}
static everyN (array, start, diff, adapter) {
const selected = []
let lastPoint = start
static everyN(array, start, diff, adapter) {
const selected = [];
let lastPoint = start;
for (let i = 0; i < array.length; i++) {
const point = array[i]
const point = array[i];
if (point - lastPoint >= diff) {
lastPoint = point
selected.push(adapter(i))
lastPoint = point;
selected.push(adapter(i));
}
}
return selected
return selected;
}
static pushAndShift (array, value, maxLength) {
array.push(value)
static pushAndShift(array, value, maxLength) {
array.push(value);
if (array.length > maxLength) {
array.splice(0, array.length - maxLength)
array.splice(0, array.length - maxLength);
}
}
}
module.exports = {
GRAPH_UPDATE_TIME_GAP,
TimeTracker
}
TimeTracker,
};

@ -1,11 +1,11 @@
function getPlayerCountOrNull (resp) {
function getPlayerCountOrNull(resp) {
if (resp) {
return resp.players.online
return resp.players.online;
} else {
return null
return null;
}
}
module.exports = {
getPlayerCountOrNull
}
getPlayerCountOrNull,
};