const minecraftJavaPing = require("mcping-js"); const minecraftBedrockPing = require("mcpe-ping-fixed"); const logger = require("./logger"); const MessageOf = require("./message"); const { TimeTracker } = require("./time"); const { getPlayerCountOrNull } = require("./util"); const config = require("../config"); function ping(serverRegistration, timeout, callback, version) { switch (serverRegistration.data.type) { case "PC": serverRegistration.dnsResolver.resolve((host, port, remainingTimeout) => { const server = new minecraftJavaPing.MinecraftServer( host, port || 25565 ); server.ping(remainingTimeout, version, (err, res) => { if (err) { callback(err); } else { const payload = { players: { online: capPlayerCount( serverRegistration.data.ip, parseInt(res.players.online) ), }, 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; } callback(null, payload); } }); }); 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); } } // player count can be up to 1^32-1, which is a massive scale and destroys browser performance when rendering graphs // Artificially cap and warn to prevent propogating garbage function capPlayerCount(host, playerCount) { const maxPlayerCount = 250000; if (playerCount !== Math.min(playerCount, maxPlayerCount)) { logger.log( "warn", "%s returned a player count of %d, Minetrack has capped it to %d to prevent browser performance issues with graph rendering. If this is in error, please edit maxPlayerCount in ping.js!", host, playerCount, maxPlayerCount ); return maxPlayerCount; } else if (playerCount !== Math.max(playerCount, 0)) { logger.log( "warn", "%s returned an invalid player count of %d, setting to 0.", host, playerCount ); return 0; } return playerCount; } class PingController { constructor(app) { this._app = app; this._isRunningTasks = false; } schedule() { setInterval(this.pingAll, config.rates.pingAll); // todo: make this a cron job? this.pingAll(); } pingAll = () => { const { timestamp, updateHistoryGraph } = this._app.timeTracker.newPointTimestamp(); this.startPingTasks((results) => { const updates = []; for (const serverRegistration of this._app.serverRegistrations) { 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); 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 ); 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, }) ); }); }; 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' ); return; } this._isRunningTasks = true; const results = []; for (const serverRegistration of this._app.serverRegistrations) { 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 ); } 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; callback(results); } }, version.protocolId ); } }; } module.exports = PingController;