diff --git a/Server/controllers/nodesController.js b/Server/controllers/nodesController.js index 7387e07..e2b3261 100644 --- a/Server/controllers/nodesController.js +++ b/Server/controllers/nodesController.js @@ -2,31 +2,42 @@ const { DebugBuilder } = require("../utilities/debugBuilder.js"); const log = new DebugBuilder("server", "nodesController"); // Utilities -const mysqlHander = require("../utilities/mysqlHandler"); +const {getAllNodes, addNewNode, updateNodeInfo, getNodeInfoFromId, getOnlineNodes } = require("../utilities/mysqlHandler"); const utils = require("../utilities/utils"); +const { sendHttpRequest, requestOptions } = require("../utilities/httpRequests.js"); +const { nodeObject } = require("../utilities/recordHelper.js"); +const refreshInterval = process.env.NODE_MONITOR_REFRESH_INTERVAL ?? 1200000; + +/** + * + * @param {*} req + * @param {*} res + */ exports.listAllNodes = async (req, res) => { - mysqlHander.getAllNodes((allNodes) => { + getAllNodes((allNodes) => { res.status(200).json({ "nodes_online": allNodes }); }); } -// Add a new node to the +// Add a new node to the storage exports.newNode = async (req, res) => { if (!req.body.name) return res.send(400) try { // Try to add the new user with defaults if missing options - mysqlHander.addNewNode({ - 'name': req.body.name, - 'ip': req.body.ip ?? null, - 'port': req.body.port ?? null, - 'location': req.body.location ?? null, - 'nearbySystems': req.body.nearbySystems ?? null, - 'online': req.body.online ?? 0 - }, (queryResults) => { + const newNode = new nodeObject({ + _name: req.body.name, + _ip: req.body.ip ?? null, + _port: req.body.port ?? null, + _location: req.body.location ?? null, + _nearbySystems: req.body.nearbySystems ?? null, + _online: req.body.online ?? 0 + }); + + addNewNode(newNode, (queryResults) => { // Send back a success if the user has been added and the ID for the client to keep track of res.status(202).json({"nodeId": queryResults.insertId}); }) @@ -44,7 +55,7 @@ exports.newNode = async (req, res) => { // Get the known info for the node specified exports.getNodeInfo = async (req, res) => { if (!req.query.id) return res.status(400).json("No id specified"); - mysqlHander.getNodeInfoFromId(req.query.id, (nodeInfo) => { + getNodeInfoFromId(req.query.id, (nodeInfo) => { res.status(200).json(nodeInfo); }) } @@ -52,29 +63,77 @@ exports.getNodeInfo = async (req, res) => { // Updates the information received from the client based on ID exports.nodeCheckIn = async (req, res) => { if (!req.body.id) return res.status(400).json("No id specified"); - mysqlHander.getNodeInfoFromId(req.body.id, (nodeInfo) => { - let nodeObject = {}; + getNodeInfoFromId(req.body.id, (nodeInfo) => { + let checkInObject = new nodeObject(); // Convert the DB systems buffer to a JSON object to be worked with nodeInfo.nearbySystems = utils.BufferToJson(nodeInfo.nearbySystems) // Convert the online status to a boolean to be worked with nodeInfo.online = nodeInfo.online !== 0; - if (req.body.name && req.body.name !== nodeInfo.name) nodeObject.name = req.body.name - if (req.body.ip && req.body.ip !== nodeInfo.ip) nodeObject.ip = req.body.ip - if (req.body.port && req.body.port !== nodeInfo.port) nodeObject.port = req.body.port - if (req.body.location && req.body.location !== nodeInfo.location) nodeObject.location = req.body.location - if (req.body.nearbySystems && JSON.stringify(req.body.nearbySystems) !== JSON.stringify(nodeInfo.nearbySystems)) nodeObject.nearbySystems = req.body.nearbySystems - if (req.body.online && req.body.online !== nodeInfo.online) nodeObject.online = req.body.online + if (req.body.name && req.body.name !== nodeInfo.name) checkInObject.name = req.body.name + if (req.body.ip && req.body.ip !== nodeInfo.ip) checkInObject.ip = req.body.ip + if (req.body.port && req.body.port !== nodeInfo.port) checkInObject.port = req.body.port + if (req.body.location && req.body.location !== nodeInfo.location) checkInObject.location = req.body.location + if (req.body.nearbySystems && JSON.stringify(req.body.nearbySystems) !== JSON.stringify(nodeInfo.nearbySystems)) checkInObject.nearbySystems = req.body.nearbySystems + if (req.body.online && req.body.online !== nodeInfo.online) checkInObject.online = req.body.online // If no changes are made tell the client - if (Object.keys(nodeObject).length === 0) return res.status(200).json("No keys updated"); + if (Object.keys(checkInObject).length === 0) return res.status(200).json("No keys updated"); - log.INFO("Updating the following keys for ID: ", req.body.id, nodeObject); + log.INFO("Updating the following keys for ID: ", req.body.id, checkInObject); // Adding the ID key to the body so that the client can double-check their ID - nodeObject.id = req.body.id; - mysqlHander.updateNodeInfo(nodeObject, () => { - return res.status(202).json({"updatedKeys": nodeObject}); + checkInObject.id = req.body.id; + updateNodeInfo(checkInObject, () => { + return res.status(202).json({"updatedKeys": checkInObject}); }) }) -} \ No newline at end of file +} + +exports.nodeMonitorService = class nodeMonitorService { + constructor() { + } + + async start(){ + // Wait for the a portion of the refresh period before checking in with the nodes, so the rest of the bot can start + await new Promise(resolve => setTimeout(resolve, refreshInterval/10)); + + log.INFO("Starting Node Monitor Service"); + // Check in before starting the infinite loop + await this.checkInWithOnlineNodes(); + + + while(true){ + // Wait for the refresh interval, then wait for the posts to return, then wait a quarter of the refresh interval to make sure everything is cleared up + await new Promise(resolve => setTimeout(resolve, refreshInterval)); + await this.checkInWithOnlineNodes(); + await new Promise(resolve => setTimeout(resolve, refreshInterval / 4)); + continue; + } + } + + async checkInWithOnlineNodes(){ + getOnlineNodes((nodes) => { + log.DEBUG("Online Nodes: ", nodes); + for (const node of nodes) { + const reqOptions = new requestOptions("/client/requestCheckIn", "GET", node.ip, node.port) + const request = sendHttpRequest(reqOptions, "", (responseObj) => { + if (responseObj) { + log.DEBUG("Response from: ", node.name, responseObj); + } + else { + log.DEBUG("No response from node, assuming it's offline"); + const offlineNode = new nodeObject({ _online: 0, _id: node.id }); + log.DEBUG("Offline node update object: ", offlineNode); + updateNodeInfo(offlineNode, (sqlResponse) => { + if (!sqlResponse) log.ERROR("No response from SQL object"); + + log.DEBUG("Updated node: ", sqlResponse); + }) + } + }) + } + return; + }); + } +} diff --git a/Server/index.js b/Server/index.js index 9c787d3..69c5cd0 100644 --- a/Server/index.js +++ b/Server/index.js @@ -1,20 +1,21 @@ +// Modules var createError = require('http-errors'); var express = require('express'); var path = require('path'); var cookieParser = require('cookie-parser'); var logger = require('morgan'); var http = require('http'); - const fs = require('fs'); require('dotenv').config(); +// Utilities const { RSSController } = require("./controllers/rssController"); const libUtils = require("./libUtils"); const deployCommands = require("./utilities/deployCommands"); - +const { nodeMonitorService } = require('./controllers/nodesController'); +// Debug const { DebugBuilder } = require("./utilities/debugBuilder"); const log = new DebugBuilder("server", "index"); -//const Discord = require('discord.js');Client, Collection, Intents const { Client, Events, @@ -23,7 +24,6 @@ const { MessageActionRow, MessageButton } = require('discord.js'); -//const client = new Discord.Client(); const client = new Client({ intents: [GatewayIntentBits.GuildMessages, GatewayIntentBits.Guilds] @@ -92,6 +92,14 @@ async function runHTTPServer() { }) } +/** + * Start the node monitoring service + */ +async function runNodeMonitorService(){ + const monitor = new nodeMonitorService(); + monitor.start(); +} + /** * Start the RSS background process */ @@ -128,6 +136,9 @@ client.on('ready', () => { log.DEBUG(`Starting HTTP Server`); runHTTPServer(); + log.DEBUG("Starting Node Monitoring Service"); + runNodeMonitorService(); + log.DEBUG("Starting RSS watcher"); runRssService(); }); diff --git a/Server/utilities/httpRequests.js b/Server/utilities/httpRequests.js index b64747c..fb035ed 100644 --- a/Server/utilities/httpRequests.js +++ b/Server/utilities/httpRequests.js @@ -39,30 +39,22 @@ exports.sendHttpRequest = function sendHttpRequest(requestOptions, data, callbac res.on('data', (data) => { const responseObject = { "statusCode": res.statusCode, - "body": data + "body": (requestOptions.method === "POST") ? JSON.parse(data) : data.toString() }; - - try { - responseObject.body = JSON.parse(responseObject.body) - } - catch (err) { - } - log.DEBUG("Response Object: ", responseObject); - return callback(responseObject); + callback(responseObject); }) }).on('error', err => { - log.ERROR('Error: ', err.message) + if (err.code === "ECONNREFUSED"){ + // Bot refused connection, assumed offline + log.WARN("Connection Refused"); + } + else log.ERROR('Error: ', err.message, err); + callback(undefined); // TODO need to handle if the server is down }) - if (requestOptions.timeout) { - req.setTimeout(requestOptions.timeout, () => { - return callback(false); - }); - } - // Write the data to the request and send it - req.write(data) - req.end() + req.write(data); + req.end(); } \ No newline at end of file diff --git a/Server/utilities/mysqlHandler.js b/Server/utilities/mysqlHandler.js index 06d21f4..2816269 100644 --- a/Server/utilities/mysqlHandler.js +++ b/Server/utilities/mysqlHandler.js @@ -68,6 +68,7 @@ exports.addNewNode = (nodeObject, callback) => { * @param callback Callback function */ exports.updateNodeInfo = (nodeObject, callback) => { + if(!nodeObject.id) throw new Error("Attempted to updated node without providing ID", nodeObject); const name = nodeObject.name, ip = nodeObject.ip, port = nodeObject.port, @@ -84,8 +85,8 @@ exports.updateNodeInfo = (nodeObject, callback) => { nearbySystems = utils.JsonToBuffer(nearbySystems) queryParams.push(`nearbySystems = '${nearbySystems}'`); } - if (typeof online === "boolean") { - if (online) queryParams.push(`online = 1`); + if (typeof online === "boolean" || typeof online === "number") { + if (online || online === 1) queryParams.push(`online = 1`); else queryParams.push(`online = 0`); } diff --git a/Server/utilities/recordHelper.js b/Server/utilities/recordHelper.js index 7105d4d..852d256 100644 --- a/Server/utilities/recordHelper.js +++ b/Server/utilities/recordHelper.js @@ -96,4 +96,31 @@ class BaseUserAccount { } } -exports.BaseUserAccount = BaseUserAccount; \ No newline at end of file +exports.BaseUserAccount = BaseUserAccount; + +/** + * + */ +class nodeObject { + /** + * + * @param {*} param0._id The ID of the node + * @param {*} param0._name The name of the node + * @param {*} param0._ip The IP that the master can contact the node at + * @param {*} param0._port The port that the client is listening on + * @param {*} param0._location The physical location of the node + * @param {*} param0._online An integer representation of the online status of the bot, ie 0=off, 1=on + * @param {*} param0._nearbySystems An object array of nearby systems + */ + constructor({ _id = null, _name = null, _ip = null, _port = null, _location = null, _nearbySystems = null, _online = null }) { + this.id = _id; + this.name = _name; + this.ip = _ip; + this.port = _port; + this.location = _location; + this.nearbySystems = _nearbySystems; + this.online = _online; + } +} + +exports.nodeObject = nodeObject; \ No newline at end of file