prettyify code
This commit is contained in:
96
lib/app.js
96
lib/app.js
@ -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;
|
||||
|
428
lib/database.js
428
lib/database.js
@ -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;
|
||||
|
83
lib/dns.js
83
lib/dns.js
@ -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,
|
||||
});
|
||||
};
|
||||
|
221
lib/ping.js
221
lib/ping.js
@ -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;
|
||||
|
143
lib/server.js
143
lib/server.js
@ -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;
|
||||
|
240
lib/servers.js
240
lib/servers.js
@ -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;
|
||||
|
102
lib/time.js
102
lib/time.js
@ -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,
|
||||
};
|
||||
|
10
lib/util.js
10
lib/util.js
@ -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,
|
||||
};
|
||||
|
Reference in New Issue
Block a user