diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index ab8ff45..9a485d5 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -5,14 +5,19 @@ on: branches: - main - 'ci*' + - 'front-end' + - 'api' pull_request: branches: - main - 'ci*' + - 'front-end' + - 'api' jobs: test-api-linux: name: API Test (Linux) + if: github.ref_name == 'api' || (github.ref_name != 'api' && github.ref_name != 'front-end') runs-on: ubuntu-latest strategy: @@ -35,6 +40,7 @@ jobs: test-frontend-linux: name: Frontend Test (Linux) + if: github.ref_name == 'front-end' || (github.ref_name != 'api' && github.ref_name != 'front-end') runs-on: ubuntu-latest strategy: @@ -57,6 +63,7 @@ jobs: test-api-windows: name: API Test (Windows) + if: github.ref_name == 'api' || (github.ref_name != 'api' && github.ref_name != 'front-end') runs-on: windows-latest strategy: @@ -79,6 +86,7 @@ jobs: test-frontend-windows: name: Frontend Test (Windows) + if: github.ref_name == 'front-end' || (github.ref_name != 'api' && github.ref_name != 'front-end') runs-on: windows-latest strategy: diff --git a/api/app.js b/api/app.js new file mode 100644 index 0000000..82f0d44 --- /dev/null +++ b/api/app.js @@ -0,0 +1,29 @@ +const express = require('express'); +const cors = require('cors'); +const { limiter } = require('./middleware/limiter.middleware'); + +const app = express(); + +const adminRoutes = require('./routes/admin.routes'); +const serverRoutes = require('./routes/server.routes'); +const propertiesRoute = require('./routes/properties.routes'); +const installationsRoutes = require('./routes/installations.routes'); +const infoRoutes = require('./routes/info.routes'); + +app.use(cors()); +app.use(limiter) +app.use(express.json()); + +app.use('/server', serverRoutes); +app.use('/installations', installationsRoutes); +app.use('/properties', propertiesRoute); +app.use('/info', infoRoutes); +app.use('/admin', adminRoutes); + +app.get('/ping', async (req, res) => { + res.send(`pong`); +}); + +module.exports = { + app +} \ No newline at end of file diff --git a/api/cleaner.js b/api/cleaner.js index 9029e3e..c5dad02 100644 --- a/api/cleaner.js +++ b/api/cleaner.js @@ -1,12 +1,9 @@ - let cleanup_done = false; -const networkingUtils = require('./utils/networkingUtils'); -const configUtils = require('./utils/configUtils'); +const networkingUtils = require('./utils/networking.util'); +const configUtils = require('./utils/config.util'); const debug = configUtils.getConfigAttribute("debug"); - async function cleanup() { - if(debug){ console.log("\nDebug mode is turned on\n Skipping cleanup tasks...\n if you don't intend this, change 'debug' from true to false in server-config.json"); console.log("====== API TERMINATED! ======"); @@ -27,20 +24,17 @@ async function cleanup() { } process.on('exit', () => { - if (!cleanup_done) { + if (!cleanup_done) cleanup(); - } }); -process.on('SIGINT', async () => { +async function handleExit() { await cleanup(); process.exit(0); -}); +} -process.on('SIGTERM', async () => { - await cleanup(); - process.exit(0); -}); +process.on('SIGINT', handleExit); +process.on('SIGTERM', handleExit); process.on('uncaughtException', async (err) => { console.error('Uncaught Exception:', err); diff --git a/api/consts.js b/api/consts.js index 9c0289a..b4b3055 100644 --- a/api/consts.js +++ b/api/consts.js @@ -14,6 +14,11 @@ const configFilePath = "./server-config.json"; const eulaFilePath = `${serverDirectory}/eula.txt`; const upnpcPath = '/upnpc'; +const serverStatus = { + OFFLINE: 0, + RUNNING: 1, + STARTING: 2 +}; module.exports = { serverDirectory, @@ -29,4 +34,5 @@ module.exports = { serverBannedIPsPath, keysJSONPath, eulaFilePath, + serverStatus }; \ No newline at end of file diff --git a/api/controllers/info.controller.js b/api/controllers/info.controller.js new file mode 100644 index 0000000..c4b9eda --- /dev/null +++ b/api/controllers/info.controller.js @@ -0,0 +1,86 @@ +const propertiesService = require('../services/properties.service'); +const infoService = require('../services/info.service'); +const serverService = require('../services/server.service'); +const consts = require('../consts'); + +async function playerCount(req, res) { + try { + const playerCount = await propertiesService.getOnlinePlayers(); + + res.status(200).send({ playerCount: playerCount }); + } catch(error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + +async function getUpTime(req, res) { + try { + + res.status(200).send(await infoService.getUpTime()); + } catch (error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + +async function getMemoryUsage(req, res) { + try { + let serverProcess = serverService.getServerProcess(); + res.status(200).send(await infoService.getMemoryUsage(serverProcess)); + } catch (error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + +async function getWorldSize(req, res) { + try { + let worldSize = await infoService.getDirectorySize(consts.serverDirectory + "/world"); + res.status(200).send({ worldSize: `${worldSize.toFixed(2)}MB` }); + } catch (error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + +async function getVersion(req, res) { + try { + let version = infoService.getVersion(consts.serverDirectory + "/" + consts.serverName); + res.status(200).send({ version: version }); + } catch (error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + +async function getPlatform(req, res) { + try { + let platform = infoService.getPlatform(consts.serverDirectory + "/" + consts.serverName); + res.status(200).send({ platform: platform }); + } catch (error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + +async function getAllInfo(req, res) { + try { + let serverProcess = serverService.getServerProcess(); + let jarPath = consts.serverDirectory + "/" + consts.serverName + res.status(200).send(await infoService.getInfo(serverProcess, jarPath, consts.serverDirectory)); + } catch (error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + +module.exports = { + playerCount, + getUpTime, + getMemoryUsage, + getWorldSize, + getVersion, + getPlatform, + getAllInfo +} \ No newline at end of file diff --git a/api/controllers/installations.controller.js b/api/controllers/installations.controller.js new file mode 100644 index 0000000..64ea930 --- /dev/null +++ b/api/controllers/installations.controller.js @@ -0,0 +1,25 @@ +const installationsService = require('../services/installations.service'); +const { updateConfigAttribute } = require('../utils/config.util'); +const { Sema } = require('async-sema'); + +const downloadSema = new Sema(1); + +async function downloadServer(req, res) { + await downloadSema.acquire(); + + try { + await installationsService.downloadRouter(req.params.platform, req.params.version); + res.status(201).send('Downloaded Successfully'); + + updateConfigAttribute("platform", req.params.platform); + updateConfigAttribute("version", req.params.version); + } catch (error) { + res.status(500).send(`Error downloading server files: ${error}`); + } finally{ + downloadSema.release(); + } +} + +module.exports = { + downloadServer +} \ No newline at end of file diff --git a/api/controllers/properties.controller.js b/api/controllers/properties.controller.js new file mode 100644 index 0000000..72d0cd9 --- /dev/null +++ b/api/controllers/properties.controller.js @@ -0,0 +1,164 @@ +const propertiesService = require("../services/properties.service"); +const configUtils = require("../utils/config.util"); +const jsonFilesUtils = require("../utils/files.util"); + +const { Sema } = require('async-sema'); + +let togglePropertySema = new Sema(1); + +async function toggleProperty(req, res) { + togglePropertySema.acquire(); + + try { + console.log(req.params.property); + await propertiesService.updateProperty(req.params.property, true); + res.status(200).send("done"); + } catch(error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } finally { + togglePropertySema.release(); + } +} + +let ramAllocationSema = new Sema(1); + +async function allocateRam(req, res) { + ramAllocationSema.acquire(); + + try { + configUtils.updateMemoryAllocated(req.params.mb, true); + res.status(200).send(`Ram allocation updated to ${req.params.mb}M`); + } catch(error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } finally { + ramAllocationSema.release(); + } +} + +async function serverConfig(req, res) { + try { + const configJSON = configUtils.getConfigJSON(); + res.status(200).send(configJSON); + } catch(error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + +async function getWhitelist(req, res) { + try { + const whitelistJSON = jsonFilesUtils.getWhitelistJSON(); + res.status(200).send(whitelistJSON); + } catch(error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + +async function getOps(req, res) { + try { + const opsJSON = jsonFilesUtils.getOpsJSON(); + res.status(200).send(opsJSON); + } catch(error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + +async function getBannedPlayers(req, res) { + try { + const bannedplayersJSON = jsonFilesUtils.getBannedPlayersJSON(); + res.status(200).send(bannedplayersJSON); + } catch(error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + +async function modifyOperator(req, res) { + try { + if (req.params.operation != "add" && req.params.operation != "remove") { + res.status(404).send(`Invalid operation ${req.params.operation}`); + return; + } + + await jsonFilesUtils.modifyOpsJSON(req.params.playername, req.params.operation === "add"); + res.status(200).send(`${req.params.operation === "add" ? "Added" : "Remove"} ${req.params.playername} as an Operator`); + } catch(error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + +async function modifyWhitelist(req, res) { + try { + if (req.params.operation != "add" && req.params.operation != "remove") { + res.status(404).send(`Invalid operation ${req.params.operation}`); + return; + } + + await jsonFilesUtils.modifyWhitelistJSON(req.params.playername, req.params.operation === "add"); + if (req.params.operation === "add") + res.status(200).send(`Added ${req.params.playername} to the Whitelist`); + else + res.status(200).send(`Remove ${req.params.playername} from the Whitelist`); + + } catch(error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + +async function modifyBanned(req, res) { + try { + if (req.params.operation != "add" && req.params.operation != "remove") { + res.status(404).send(`Invalid operation ${req.params.operation}`); + return; + } + + await jsonFilesUtils.modifyBannedPlayersJSON(req.params.playername, req.params.operation === "add"); + if (req.params.operation === "add") + res.status(200).send(`Banned ${req.params.playername}`); + else + res.status(200).send(`Pardoned ${req.params.playername}`); + + } catch(error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + +async function modifyBannedIPs(req, res) { + try { + if (req.params.operation != "add" && req.params.operation != "remove") { + res.status(404).send(`Invalid operation ${req.params.operation}`); + return; + } + + await jsonFilesUtils.modifyBannedIPsJSON(req.params.playername, req.params.operation === "add"); + if (req.params.operation === "add") + res.status(200).send(`Banned ${req.params.playername}`); + else + res.status(200).send(`Pardoned ${req.params.playername}`); + + } catch(error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +} + + +module.exports = { + toggleProperty, + allocateRam, + serverConfig, + getWhitelist, + getOps, + getBannedPlayers, + modifyOperator, + modifyWhitelist, + modifyBanned, + modifyBannedIPs +} \ No newline at end of file diff --git a/api/controllers/server.controller.js b/api/controllers/server.controller.js new file mode 100644 index 0000000..c997658 --- /dev/null +++ b/api/controllers/server.controller.js @@ -0,0 +1,137 @@ +const serverService = require('../services/server.service'); +const configUtils = require('../utils/config.util'); +const infoService = require('../services/info.service'); + +const { Sema } = require('async-sema'); + +const consoleSema = new Sema(1); +async function runCommand(req, res) { + await consoleSema.acquire(); + + try { + await serverService.runMCCommand(req.params.command); + + res.status(200).send("done"); + } catch(error) { + console.error(error); + + res.status(500).send("Error: " + error.message); + } finally { + consoleSema.release(); + } +} + +const startStopSema = new Sema(1); +async function startServer(req, res) { + await startStopSema.acquire(); + try { + if (await serverService.isServerOn()) { + res.status(400).send('Server is already running.'); + return; + } + if (!serverService.isEULAsigned()){ + res.status(400).send('EULA must be signed.'); + return; + } + serverService.serverStatus = 2; + + if( configUtils.getConfigAttribute("start_with_script") ){ + await serverService.startServerWithScript(); + }else{ + await serverService.startServer(); + } + infoService.startCounting(); + res.send('Server started.'); + + } catch (error) { + serverService.serverStatus = 0; + res.status(500).send(`Error starting server: ${error}`); + } finally { + startStopSema.release(); + // console.log("startstop sema RELEASED"); + } +} + +async function stopServer(req, res) { + await startStopSema.acquire(); + + try { + if (!(await serverService.isServerOn())) + return res.status(400).send('Server is not running.'); + + try { + await serverService.runMCCommand("stop"); + } catch { + await serverService.killStrayServerInstance(); + } finally { + serverService.deleteServerOutput(); + serverStatus = 0; + } + + infoService.stopCounting(); + res.status(200).send('Server stopped.'); + } catch (error) { + res.status(500).send(`Failed to stop server: ${error}`); + } finally { + startStopSema.release(); + } +} + +async function getServerConsoleOutput(req ,res) { + try { + const consoleOutput = serverService.getServerlogs(); + + return res.status(200).send(consoleOutput ?? "The server is offline..."); + } catch (error) { + res.status(500).send(); + } +} + +async function checkExist(req, res) { + try{ + const response = Boolean(serverService.doesServerJarAlreadyExist()); + res.status(200).send(response); + } + catch(error){ + res.status(500).send("Internal Server error while checking files"); + } +} + +async function checkServerStatus(req, res) { + try { + const status = serverService.isServerStarting(); + + const statusMap = { + 0: "0", // Offline (default) + 1: "1", // Running + 2: "2" // Starting + }; + + res.status(200).send(statusMap[status] ?? "0"); + } catch (error) { + console.error("Error checking server:", error); + res.status(500).send("Internal Server Error"); + } +} + +async function signEULA(req, res) { + try { + if (serverService.isEULAsigned()){ + res.status(400).send('EULA is already signed.'); + } + serverService.signEULA(); + res.status(200).send('EULA signed.'); + } catch (error) { + res.status(500).send(`Failed to sign EULA: ${error}`); + } +} + +module.exports = { + runCommand, + startServer, + getServerConsoleOutput, + checkExist, + checkServerStatus, + stopServer, + signEULA +} \ No newline at end of file diff --git a/api/middleware/limiter.middleware.js b/api/middleware/limiter.middleware.js new file mode 100644 index 0000000..7c2b150 --- /dev/null +++ b/api/middleware/limiter.middleware.js @@ -0,0 +1,14 @@ +const rateLimit = require("express-rate-limit"); + +const limiter = rateLimit({ + max: 100, // maximum of 10 requests per window (15 sec) + windowMs: 15 * 1000, // Window : 15 seconds + message: "You are being rate-limited.", + headers: { + "Retry-After": 15 + } +}); + +module.exports = { + limiter +} \ No newline at end of file diff --git a/api/middleware/middleware.js b/api/middleware/middleware.js deleted file mode 100644 index 123bb60..0000000 --- a/api/middleware/middleware.js +++ /dev/null @@ -1,36 +0,0 @@ - -const cors = require('cors'); - -const apiKeyValidation = (req, res, next) => { - const apiKey = req.header('x-api-key'); - const apiName = req.header('x-api-name'); - if ((apiKey && - apiAccessUtils.doesEntryExist(apiName, apiKey) - ) - || debug === true) { - console.log("API Key Validation Passed for", req.ip); - next(); - } else { - console.log("API Key Validation Failed for", req.ip); - res.status(401).json({ error: 'Unauthorized' }); - } - }; - - -const rateLimit = require("express-rate-limit"); - -const limiter = rateLimit({ - max: 100, // maximum of 10 requests per window (15 sec) - windowMs: 15 * 1000, // Window : 15 seconds - message: "You are being rate-limited.", - headers: { - "Retry-After": 15 - } -}); - -module.exports = { - limiter, - apiKeyValidation, - cors, - -}; \ No newline at end of file diff --git a/api/package-lock.json b/api/package-lock.json index a38c4b8..85f1102 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -9,11 +9,13 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "adm-zip": "^0.5.16", "async-sema": "^3.1.1", "cors": "^2.8.5", "express": "^4.21.2", "express-rate-limit": "^7.5.0", - "nodemon": "^3.1.9" + "nodemon": "^3.1.9", + "pidusage": "^4.0.1" } }, "node_modules/accepts": { @@ -29,6 +31,15 @@ "node": ">= 0.6" } }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -870,6 +881,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidusage": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-4.0.1.tgz", + "integrity": "sha512-yCH2dtLHfEBnzlHUJymR/Z1nN2ePG3m392Mv8TFlTP1B0xkpMQNHAnfkY0n2tAi6ceKO6YWhxYfZ96V4vVkh/g==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", diff --git a/api/package.json b/api/package.json index b5f72f9..7f1793c 100644 --- a/api/package.json +++ b/api/package.json @@ -17,10 +17,12 @@ "license": "ISC", "description": "", "dependencies": { + "adm-zip": "^0.5.16", "async-sema": "^3.1.1", "cors": "^2.8.5", "express": "^4.21.2", "express-rate-limit": "^7.5.0", - "nodemon": "^3.1.9" + "nodemon": "^3.1.9", + "pidusage": "^4.0.1" } } diff --git a/api/routes/adminRoutes.js b/api/routes/admin.routes.js similarity index 96% rename from api/routes/adminRoutes.js rename to api/routes/admin.routes.js index 3c96641..fed5f2a 100644 --- a/api/routes/adminRoutes.js +++ b/api/routes/admin.routes.js @@ -1,4 +1,4 @@ -const apiAccessUtils = require('../utils/apiAccessUtils'); +const apiAccessUtils = require('../utils/access.util'); const express = require('express'); const router = express.Router(); diff --git a/api/routes/info.routes.js b/api/routes/info.routes.js new file mode 100644 index 0000000..328a956 --- /dev/null +++ b/api/routes/info.routes.js @@ -0,0 +1,28 @@ +const express = require('express'); +const router = express.Router(); + +const { + playerCount, + getUpTime, + getMemoryUsage, + getWorldSize, + getVersion, + getPlatform, + getAllInfo +} = require('../controllers/info.controller'); + +router.get('/', getAllInfo); + +router.get('/player-count', playerCount); + +router.get('/uptime', getUpTime); + +router.get('/memory-usage', getMemoryUsage); + +router.get('/world-size', getWorldSize); + +router.get('/version', getVersion); + +router.get('/platform', getPlatform); + +module.exports = router; \ No newline at end of file diff --git a/api/routes/installations.routes.js b/api/routes/installations.routes.js new file mode 100644 index 0000000..f510ade --- /dev/null +++ b/api/routes/installations.routes.js @@ -0,0 +1,8 @@ +const express = require('express'); +const router = express.Router(); + +const { downloadServer } = require('../controllers/installations.controller') + +router.put('/download/:platform/:version', downloadServer); + +module.exports = router; \ No newline at end of file diff --git a/api/routes/installationsRoutes.js b/api/routes/installationsRoutes.js deleted file mode 100644 index bf9351a..0000000 --- a/api/routes/installationsRoutes.js +++ /dev/null @@ -1,29 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const installationsUtils = require('../utils/installationsUtils'); -const { updateConfigAttribute } = require('../utils/configUtils'); - -const { Sema } = require('async-sema'); - -const downloadSema = new Sema(1); - -router.put('/download/:platform/:version', async (req, res) => { - await downloadSema.acquire(); - - try { - await installationsUtils.downloadRouter(req.params.platform, req.params.version); - res.status(201).send('Downloaded Successfully'); - - updateConfigAttribute("platform", req.params.platform); - updateConfigAttribute("version", req.params.version); - - } catch (error) { - - res.status(500).send(`Error downloading server files: ${error}`); - } finally{ - downloadSema.release(); - } -}); - - -module.exports = router; \ No newline at end of file diff --git a/api/routes/properties.routes.js b/api/routes/properties.routes.js new file mode 100644 index 0000000..812026c --- /dev/null +++ b/api/routes/properties.routes.js @@ -0,0 +1,48 @@ +const express = require('express'); +const router = express.Router(); +const propertiesServices = require('../services/properties.service'); + +const { + toggleProperty, + allocateRam, + serverConfig, + getWhitelist, + getOps, + getBannedPlayers, + modifyOperator, + modifyWhitelist, + modifyBanned, + modifyBannedIPs + } = require("../controllers/properties.controller"); + +router.get('/', async (req, res) => { + try { + const properties = await propertiesServices.getProperties(); + res.status(200).contentType("json").send(properties); + } catch(error) { + console.error(error); + res.status(500).send("error.. " + error.message); + } +}) + +router.put('/toggle/:property', toggleProperty); + +router.put('/allocate-ram/:mb', allocateRam); + +router.get('/server-config.json', serverConfig); + +router.get('/whitelist.json', getWhitelist); + +router.get('/ops.json', getOps); + +router.get('/banned-players.json', getBannedPlayers); + +router.put('/:operation/op/:playername', modifyOperator); + +router.put('/:operation/whitelist/:playername', modifyWhitelist); + +router.put('/:operation/ban/:playername', modifyBanned); + +router.put('/:operation/ban-ip/:ip', modifyBannedIPs); + +module.exports = router; \ No newline at end of file diff --git a/api/routes/propertiesRoutes.js b/api/routes/propertiesRoutes.js deleted file mode 100644 index 8d33484..0000000 --- a/api/routes/propertiesRoutes.js +++ /dev/null @@ -1,179 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const propertiesUtils = require('../utils/propertiesUtils'); -const configUtils = require("../utils/configUtils"); -const jsonFilesUtils = require("../utils/jsonFilesUtils"); - - -const { Sema } = require('async-sema'); - -let togglePropertySema = new Sema(1); -let ramAllocationSema = new Sema(1); - - -router.get('/', async (req, res) => { - try{ - const properties = await propertiesUtils.getProperties(); - res.status(200).contentType("json").send(properties); - }catch(error){ - console.error(error) - res.status(500).send("error.. " + error.message); - } -}) - - -router.put('/toggle/:property', async (req, res) => { - togglePropertySema.acquire() - try{ - console.log(req.params.property); - await propertiesUtils.updateProperty(req.params.property, true); - res.status(200).send("done"); - }catch(error){ - console.error(error) - res.status(500).send("error.. " + error.message); - }finally{ - togglePropertySema.release() - } -}) - - -router.put('/allocate-ram/:mb', async (req, res) => { - ramAllocationSema.acquire() - try{ - configUtils.updateMemoryAllocated(req.params.mb, true); - res.status(200).send(`Ram allocation updated to ${req.params.mb}M`); - }catch(error){ - console.error(error) - res.status(500).send("error.. " + error.message); - }finally{ - ramAllocationSema.release() - } -}) - -router.get('/player-count', async (req, res) => { - try{ - const playerCount = await propertiesUtils.getOnlinePlayers() - res.status(200).send({playerCount: playerCount}); - }catch(error){ - console.error(error) - res.status(500).send("error.. " + error.message); - } -}) - -router.get('/server-config.json', async (req, res) => { - try{ - const configJSON = configUtils.getConfigJSON(); - res.status(200).send(configJSON); - }catch(error){ - console.error(error) - res.status(500).send("error.. " + error.message); - } -}) -router.get('/whitelist.json', async (req, res) => { - try{ - const whitelistJSON = propertiesUtils.getWhitelistJSON(); - res.status(200).send(whitelistJSON); - }catch(error){ - console.error(error) - res.status(500).send("error.. " + error.message); - } -}) -router.get('/ops.json', async (req, res) => { - try{ - const opsJSON = propertiesUtils.getOpsJSON(); - res.status(200).send(opsJSON); - }catch(error){ - console.error(error) - res.status(500).send("error.. " + error.message); - } -}) -router.get('/banned-players.json', async (req, res) => { - try{ - const bannedplayersJSON = propertiesUtils.getBannedPlayersJSON(); - res.status(200).send(bannedplayersJSON); - }catch(error){ - console.error(error) - res.status(500).send("error.. " + error.message); - } -}) -router.put('/:operation/op/:playername', async (req, res) => { - try{ - if(req.params.operation === "add"){ - await jsonFilesUtils.modifyOpsJSON(req.params.playername, true); - res.status(200).send(`Added ${req.params.playername} as an Operator`); - - } else if(req.params.operation === "remove") { - await jsonFilesUtils.modifyOpsJSON(req.params.playername, false); - res.status(200).send(`Remove ${req.params.playername} as an Operator`); - - } else { - res.status(404).send(`Invalid operation ${req.params.operation}`); - } - - }catch(error){ - console.error(error) - res.status(500).send("error.. " + error.message); - } -}) - -router.put('/:operation/whitelist/:playername', async (req, res) => { - try{ - if(req.params.operation === "add"){ - await jsonFilesUtils.modifyWhitelistJSON(req.params.playername, true); - res.status(200).send(`Added ${req.params.playername} as an Operator`); - - } else if(req.params.operation === "remove") { - await jsonFilesUtils.modifyWhitelistJSON(req.params.playername, false); - res.status(200).send(`Remove ${req.params.playername} as an Operator`); - - } else { - res.status(404).send(`Invalid operation ${req.params.operation}`); - } - - }catch(error){ - console.error(error) - res.status(500).send("error.. " + error.message); - } -}) - -router.put('/:operation/ban/:playername', async (req, res) => { - try{ - if(req.params.operation === "add"){ - await jsonFilesUtils.modifyBannedPlayersJSON(req.params.playername, true); - res.status(200).send(`Banned ${req.params.playername} `); - - } else if(req.params.operation === "remove") { - await jsonFilesUtils.modifyBannedPlayersJSON(req.params.playername, false); - res.status(200).send(`Pardoned ${req.params.playername}`); - - } else { - res.status(404).send(`Invalid operation ${req.params.operation}`); - } - - }catch(error){ - console.error(error) - res.status(500).send("error.. " + error.message); - } -}) - -router.put('/:operation/ban-ip/:ip', async (req, res) => { - try{ - if(req.params.operation === "add"){ - await jsonFilesUtils.modifyBannedIPsJSON(req.params.ip, true); - res.status(200).send(`Banned ${req.params.ip} `); - - } else if(req.params.operation === "remove") { - await jsonFilesUtils.modifyBannedIPsJSON(req.params.ip, false); - res.status(200).send(`Pardoned ${req.params.ip}`); - - } else { - res.status(404).send(`Invalid operation ${req.params.operation}`); - } - - }catch(error){ - console.error(error) - res.status(500).send("error.. " + error.message); - } -}) - -module.exports = router; \ No newline at end of file diff --git a/api/routes/server.routes.js b/api/routes/server.routes.js new file mode 100644 index 0000000..c2fba49 --- /dev/null +++ b/api/routes/server.routes.js @@ -0,0 +1,31 @@ +const express = require('express'); +const router = express.Router(); + +const { + runCommand, + startServer, + getServerConsoleOutput, + checkExist, + checkServerStatus, + stopServer, + signEULA +} = require('../controllers/server.controller'); + +router.put('/console/run/:command', runCommand); + +router.get('/console-text', getServerConsoleOutput); + +router.get('/check-exist', checkExist) + +// Route to check if the server is running +router.get('/check-server', checkServerStatus); + +// Route to start the server +router.put('/start', startServer); + +// Route to stop the server +router.put('/stop', stopServer); + +router.put('/sign-eula', signEULA); + +module.exports = router; \ No newline at end of file diff --git a/api/routes/serverRoutes.js b/api/routes/serverRoutes.js deleted file mode 100644 index 809b0a7..0000000 --- a/api/routes/serverRoutes.js +++ /dev/null @@ -1,133 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const serverUtils = require('../utils/serverUtils'); -const { Sema } = require('async-sema'); - -const consoleSema = new Sema(1); - -router.put('/console/run/:command', async (req, res) => { - await consoleSema.acquire(); - try{ - await serverUtils.runMCCommand(req.params.command); - res.status(200).send("done"); - }catch(error){ - console.error(error) - res.status(500).send("error.. "+error.message); - }finally{ - consoleSema.release(); - } -}) - -router.get('/console-text', async (req, res) =>{ - try { - const consoleOutput = serverUtils.getServerlogs(); - if(consoleOutput == null){ - return res.status(200).send("The server is offline..."); - }else{ - return res.status(200).send(consoleOutput); - } - } catch (error) { - res.status(500).send(); - } -}); - - - -router.get('/check-exist', (req, res) => { - try{ - const response = Boolean(serverUtils.doesServerJarAlreadyExist()); - res.status(200).send(response); - } - catch(error){ - res.status(500).send("Internal Server error while checking files"); - } -}) - -// Route to check if the server is running -router.get('/check-server', async (req, res) => { - try { - const check = serverUtils.isServerStarting(); - switch(check) { - case 2: - // console.log("Server is starting..."); - res.status(200).send("2"); // 2 means starting - break; - case 1: - // console.log("Server is running..."); - res.status(200).send("1"); // 1 means On - break; - default: - // console.log("Server is offline..."); - res.status(200).send("0"); // 0 means Off - } - } catch (error) { - res.status(500).send(`Error checking server: ${error}`); - } -}); - -const startStopSema = new Sema(1); - -// Route to start the server -router.put('/start', async (req, res) => { - await startStopSema.acquire(); - try { - if (await serverUtils.isServerOn()) { - res.status(400).send('Server is already running.'); - return; - } - if (!serverUtils.isEULAsigned()){ - res.status(400).send('EULA must be signed.'); - return; - } - serverUtils.serverStatus = 2; - - await serverUtils.startServerWithScript(); - res.send('Server started.'); - } catch (error) { - serverUtils.serverStatus = 0; - res.status(500).send(`Error starting server: ${error}`); - } finally { - startStopSema.release() - // console.log("startstop sema RELEASED") - } -}); - -// Route to stop the server -router.put('/stop', async (req, res) => { - await startStopSema.acquire(); - try { - if (!(await serverUtils.isServerOn())) { - return res.status(400).send('Server is not running.'); - } - - try { - await serverUtils.runMCCommand("stop"); - } catch { - await serverUtils.killStrayServerInstance(); - } - finally { - serverUtils.deleteServerOutput(); - serverStatus = 0; - } - - res.status(200).send('Server stopped.'); - } catch (error) { - res.status(500).send(`Failed to stop server: ${error}`); - } finally{ - startStopSema.release(); - } -}); - -router.put('/sign-eula', async (req, res) => { - try { - if (serverUtils.isEULAsigned()){ - res.status(400).send('EULA is already signed.'); - } - serverUtils.signEULA(); - res.status(200).send('EULA signed.'); - } catch (error) { - res.status(500).send(`Failed to sign EULA: ${error}`); - } -}); - -module.exports = router; \ No newline at end of file diff --git a/api/server.js b/api/server.js index 80b6958..56bd281 100644 --- a/api/server.js +++ b/api/server.js @@ -1,50 +1,14 @@ -const express = require('express'); -const middleware = require('./middleware/middleware'); - -const app = express(); const http = require('http'); - -app.use(middleware.cors()); -app.use(middleware.limiter) -app.use(express.json()); -// app.use(middleware.apiKeyValidation); - -// Route files -const adminRoutes = require('./routes/adminRoutes'); -const configUtils = require('./utils/configUtils') -const serverRoutes = require('./routes/serverRoutes'); // Routes are separated -const propertiesRoute = require('./routes/propertiesRoutes'); // Routes are separated -const installationsRoutes = require('./routes/installationsRoutes'); // Routes are separated +const { app } = require('./app'); +const configUtils = require('./utils/config.util') // Util files const starter = require('./starter'); const cleaner = require('./cleaner'); // [ DO NOT REMOVE] Not explicitly used, but implicitly used. -// uses process object to catch and handle exit signals and cleanup tasks - -// Routes -app.use('/server', serverRoutes); -app.use('/installations', installationsRoutes); -app.use('/properties', propertiesRoute); -app.use('/admin', adminRoutes); - -// app.get('/ping', async (req, res) => { -// res.send(`pong from ${await require("./utils/networkingUtils").getIP(local=false)}`); -// }); - -app.get('/ping', async (req, res) => { - res.send(`pong`); -}); - const api_port = configUtils.getConfigAttribute("api_port"); const mc_port = configUtils.getConfigAttribute("mc_port"); - -// // Listen on IPv4 -// http.createServer(app).listen(api_port, '0.0.0.0', () => { -// console.log(`Listening on IPv4 0.0.0.0:${api_port}`); -// }); - // Listen on IPv6 http.createServer(app).listen(api_port, '::', async () => { console.log(`Listening on IPv6 [::]:${api_port}`); diff --git a/api/services/info.service.js b/api/services/info.service.js new file mode 100644 index 0000000..edc0e8e --- /dev/null +++ b/api/services/info.service.js @@ -0,0 +1,195 @@ +const fs = require('fs'); +const path = require('path'); +const pidusage = require('pidusage') +const AdmZip = require('adm-zip'); +const consts = require('../consts'); + +let startTime = null; + +function startCounting() { + startTime = Date.now(); +} + +function stopCounting() { + startTime = null; +} + +function getStartTime() { + return startTime; +} + +async function getMemoryUsage(serverProcess) { + if (!serverProcess || !serverProcess.pid) + return { error: 'Server process not running or PID unavailable' }; + + try { + const stats = await pidusage(serverProcess.pid); + return { + usedMB: Math.round(stats.memory / 1024 / 1024), + cpu: Math.round(Number(stats.cpu.toFixed(1)) / 10 * 10) / 10 + '%', + }; + } catch (err) { + return { error: 'Failed to get usage stats: ' + err.message }; + } +} + +function getDirectorySize(folderPath) { + let totalSize = 0; + + function walkDir(currentPath) { + const entries = fs.readdirSync(currentPath, { withFileTypes: true }); + + for (const entry of entries) { + const fullPath = path.join(currentPath, entry.name); + const stats = fs.statSync(fullPath); + + if (entry.isFile()) { + totalSize += stats.size; + } else if (entry.isDirectory()) { + walkDir(fullPath); // Recursively walk into the directory + } + } + } + + walkDir(folderPath); + + const sizeInMB = totalSize / (1024 * 1024); + return sizeInMB; + } + +function getPlatform(jarPath = consts.serverDirectory + "/" + consts.serverName) { + const zip = new AdmZip(jarPath); + const entries = zip.getEntries(); + const names = entries.map(e => e.entryName); + + const has = (file) => names.includes(file); + + // Detect Paper + if ( + names.some(name => name.startsWith("paperclip/")) || + names.some(name => name.startsWith("io/papermc/paperclip/")) + ) return "Paper"; + + // Detect Forge + if ( + has("patch.properties") || has("META-INF/mods.toml") || + has("bootstrap-shim.properties") || + names.some(name => name.startsWith("net/minecraftforge/bootstrap/")) + ) return "Forge"; + + // Detect Fabric + if ( + has("fabric.mod.json") || has("META-INF/fabric.mod.json") || + names.some(name => name.startsWith("net/fabricmc/")) + ) return "Fabric"; + + return "Vanilla"; +} + +function getVersion(jarPath) { + + const zip = new AdmZip(jarPath); + const entries = zip.getEntries(); + + if (getPlatform(jarPath) == "Fabric") { + const installEntry = entries.find(entry => entry.entryName === 'install.properties'); + const text = zip.readAsText(installEntry); + return text.split("game-version=")[1]; + } + + const versionEntry = entries.find(e => e.entryName === "version.json"); + if (versionEntry) { + try { + const content = JSON.parse(zip.readAsText(versionEntry)); + return content.name || content.id || null; + } catch (err) { + console.warn("Failed to parse version.json:", err.message); + } + } + + const manifestEntry = entries.find(e => e.entryName === "META-INF/MANIFEST.MF"); + if (manifestEntry) { + const text = zip.readAsText(manifestEntry); + const match = text.match(/Implementation-Version:\s*(.*)/); + if (match) return match[1].trim(); + } + + return null; +} + +async function getUpTime() { + if (getStartTime() === null) + return { uptime: "0s" }; + + const ms = Date.now() - getStartTime(); + + const totalSeconds = Math.floor(ms / 1000); + const hours = Math.floor(totalSeconds / 3600); + const minutes = Math.floor((totalSeconds % 3600) / 60); + const seconds = totalSeconds % 60; + + let formatted = []; + if (hours > 0) formatted.push(`${hours}h`); + if (minutes > 0 || hours > 0) formatted.push(`${minutes}m`); + formatted.push(`${seconds}s`); + + return { uptime: formatted.join(" ") }; +} + +async function getInfo(serverProcess, jarPath, folderPath) { + let memoryUsage = null; + let platform = null; + let version = null; + let directorySizeMB = null; + + try { + memoryUsage = await getMemoryUsage(serverProcess); + } catch (err) { + console.warn('Failed to get memory usage:', err.message); + } + + try { + platform = getPlatform(jarPath); + } catch (err) { + console.warn('Failed to get platform:', err.message); + } + + try { + version = getVersion(jarPath); + } catch (err) { + console.warn('Failed to get version:', err.message); + } + + try { + directorySizeMB = getDirectorySize(folderPath); + directorySizeMB = Math.round(directorySizeMB * 100) / 100; + } catch (err) { + console.warn('Failed to get directory size:', err.message); + } + + try { + uptime = await getUpTime(); + } catch (err) { + console.warn('Failed to calculate uptime:', err.message); + } + + return { + memoryUsage, + platform, + version, + directorySizeMB, + uptime: uptime.uptime, + }; +} + +module.exports = { + startCounting, + stopCounting, + getStartTime, + getMemoryUsage, + getDirectorySize, + getPlatform, + getVersion, + getInfo, + getUpTime +} \ No newline at end of file diff --git a/api/services/installations.service.js b/api/services/installations.service.js new file mode 100644 index 0000000..ee1d8b2 --- /dev/null +++ b/api/services/installations.service.js @@ -0,0 +1,77 @@ +const urlFetcher = require("../utils/url_fetcher.util"); +const infoService = require('../services/info.service'); +const consts = require('../consts'); +const { writeDownloadedFile } = require("../utils/installations.util"); +const fs = require('fs/promises'); +const path = require('path'); + +const preserveList = [ + "world", + "world_nether", + "world_the_end", + "banned-ips.json", + "banned-players.json", + "ops.json", + "whitelist.json" +]; + +async function purgeServer(preserveList) { + let folderPath = consts.serverDirectory; + try { + const entries = await fs.readdir(folderPath, { withFileTypes: true }); + + for (const entry of entries) { + if (preserveList.includes(entry.name)) continue; + + const fullPath = path.join(folderPath, entry.name); + if (entry.isDirectory()) + await fs.rm(fullPath, { recursive: true, force: true }); + else + await fs.unlink(fullPath); + } + + console.log(`Deleted everything in ${folderPath} except preserved items.`); + } catch (err) { + console.error(`Error cleaning folder ${folderPath}:`, err.message); + } +} + +async function downloadRouter(platform, version) { + const oldPlatform = infoService.getPlatform(); + + const comingFromModded = oldPlatform === "fabric" || oldPlatform === "forge"; + const currentPreserveList = comingFromModded + ? preserveList.slice(3) // delete worlds + : preserveList; // keep everything + + if (oldPlatform != platform) + await purgeServer(currentPreserveList); + + let response; + try { + switch (platform) { + case "vanilla": + response = await fetch(await urlFetcher.fetchVanillaURL(version)); + break; + case "paper": + response = await fetch(await urlFetcher.fetchPaperURL(version)); + break; + case "fabric": + response = await fetch(await urlFetcher.fetchFabricURL(version)); + break; + case "forge": + response = await fetch(await urlFetcher.fetchForgeURL(version)); + break; + default: + throw new Error(`Invalid platform --> ${platform}`); + } + + await writeDownloadedFile(response, version, platform.toUpperCase()); + } catch (error) { + console.error(error); + } +} + +module.exports = { + downloadRouter +} \ No newline at end of file diff --git a/api/utils/propertiesUtils.js b/api/services/properties.service.js similarity index 98% rename from api/utils/propertiesUtils.js rename to api/services/properties.service.js index 50b3314..cfabece 100644 --- a/api/utils/propertiesUtils.js +++ b/api/services/properties.service.js @@ -1,6 +1,6 @@ const consts = require("../consts"); const fs = require("fs"); -const { getConfigAttribute } = require("./configUtils"); +const { getConfigAttribute } = require("../utils/config.util"); const { spawn } = require('child_process'); async function getProperties(){ @@ -70,7 +70,6 @@ function JSONToProperties(json){ return properties; } - async function getOnlinePlayers() { const port = getConfigAttribute("port"); @@ -104,7 +103,6 @@ async function getOnlinePlayers() { }); } - module.exports = { getProperties, updateProperty, diff --git a/api/utils/serverUtils.js b/api/services/server.service.js similarity index 70% rename from api/utils/serverUtils.js rename to api/services/server.service.js index 3f556d1..4eab5c8 100644 --- a/api/utils/serverUtils.js +++ b/api/services/server.service.js @@ -1,21 +1,22 @@ const { spawn, exec } = require('child_process'); const fs = require('fs'); const consts = require("../consts"); -const {freemem} = require('os'); -const {getConfigAttribute} = require("./configUtils"); +const { freemem } = require('os'); +const configUtils = require('../utils/config.util'); let serverProcess = null; -let serverStatus = 0; +let serverStatus = consts.serverStatus.OFFLINE; async function isServerOn() { try { - const serverPID = getConfigAttribute("os") != "Linux" ? await getStrayServerInstance_WINDOWS() : await getStrayServerInstance_LINUX(); - return !!serverPID || serverStatus == 1; + const serverPID = configUtils.getConfigAttribute("os") != "Linux" ? await getStrayServerInstance_WINDOWS() : await getStrayServerInstance_LINUX(); + return serverPID || serverStatus == consts.serverStatus.RUNNING; } catch (error) { return false; } } -function deleteServerOutput(){ + +function deleteServerOutput() { try { fs.rmSync(consts.serverLogsFilePath, { force: true }); } catch (error) { @@ -23,33 +24,30 @@ function deleteServerOutput(){ } } -function isServerStarting(){ - +function isServerStarting() { const serverLogs = getServerlogs(); - if(serverLogs == null || serverLogs.includes("All dimensions are saved")){ + + if (serverLogs == null || serverLogs.includes("All dimensions are saved")) return 0; - } - if(serverLogs.includes("Starting") && !serverLogs.includes("Done")){ - serverStatus = 2; + + if (serverLogs.includes("Starting") && !serverLogs.includes("Done")) { + serverStatus = consts.serverStatus.STARTING; return serverStatus; - }else if(serverLogs.includes("Done")){ - serverStatus = 1; + } else if (serverLogs.includes("Done")) { + serverStatus = consts.serverStatus.RUNNING; return serverStatus; - } - else{ + } else{ return 0; } } function isEULAsigned() { if (fs.existsSync(consts.eulaFilePath)){ - if (fs.readFileSync(consts.eulaFilePath, 'utf8').includes('eula=true')) { + if (fs.readFileSync(consts.eulaFilePath, 'utf8').includes('eula=true')) return true; - } else return false; - } - else + } else return false; } @@ -59,44 +57,24 @@ function signEULA() { function getServerlogs() { try { - return fs.readFileSync(consts.serverLogsFilePath, { encoding: 'utf8', flag: 'r' }) + return fs.readFileSync(consts.serverLogsFilePath, { encoding: 'utf8', flag: 'r' }); } catch (error) { return null; } } async function runMCCommand(command) { - try{ - - if(!isServerOn()){ - throw new Error("Can't run command, server is offline.") + try { + if (!isServerOn()) { + throw new Error("Can't run command, server is offline."); } + serverProcess.stdin.write(`${command}\n`); - } catch(error){ + } catch(error) { console.error(error); } } -async function killStrayServerInstance(){ - switch(getConfigAttribute("os")){ - case "Windows_NT": - await killStrayServerInstance_WINDOWS(); - serverStatus = 0; - deleteServerOutput(); - break; - case "Linux": - await killStrayServerInstance_LINUX(); - serverStatus = 0; - deleteServerOutput(); - break; - - case _: - await killStrayServerInstance_LINUX(); - break; - } -} - - function getStrayServerInstance_WINDOWS() { return new Promise((resolve, reject) => { const tasklist = spawn('tasklist', ['/FI', 'IMAGENAME eq java.exe']); @@ -111,9 +89,11 @@ function getStrayServerInstance_WINDOWS() { }); tasklist.on('close', (code) => { - if (code !== 0) { + if (code !== 0) reject(`tasklist command failed with code ${code}`); - } else if (output.includes('java.exe')) { + else if (!output.includes('java.exe')) + reject('No Minecraft server found with java.exe'); + else { const netstat = spawn('netstat', ['-ano']); let netstatOutput = ''; @@ -126,50 +106,25 @@ function getStrayServerInstance_WINDOWS() { }); netstat.on('close', (code) => { - if (code !== 0) { + if (code !== 0) reject(`netstat command failed with code ${code}`); - } else { + else { + const port = configUtils.getConfigAttribute("port"); - const port = getConfigAttribute("port"); const regex = new RegExp(`TCP\\s+.*:${port}\\s+.*\\s+LISTENING\\s+(\\d+)`, 'i'); const match = netstatOutput.match(regex); - if (match) { + if (match) resolve(parseInt(match[1])); - } else { + else reject(`No Minecraft server found on port ${port}`); - } } }); - } else { - reject('No Minecraft server found with java.exe'); } }); }); } -async function killStrayServerInstance_WINDOWS() { - try { - const strayServerPID = await getStrayServerInstance_WINDOWS(); - const command = `taskkill /PID ${strayServerPID} /F`; - - exec(command, (error, stdout, stderr) => { - if (error) { - console.error(`exec error: ${error}`); - return; - } - if (stderr) { - console.error(`stderr: ${stderr}`); - return; - } - console.log(`Process with PID ${strayServerPID} killed successfully`); - }); - } catch (error) { - console.error(error); - } -} - - function getStrayServerInstance_LINUX() { return new Promise((resolve, reject) => { // Step 1: Get all Java process PIDs @@ -185,14 +140,12 @@ function getStrayServerInstance_LINUX() { }); ps.on('close', (code) => { - if (code !== 0) { + if (code !== 0) return reject(`ps command failed with code ${code}`); - } const javaPIDs = output.trim().split('\n').map(line => line.trim()).filter(Boolean); - if (javaPIDs.length === 0) { + if (javaPIDs.length === 0) return reject('No Java processes found.'); - } // Step 2: Check for listeners on the target port const ss = spawn('ss', ['-tlnp']); @@ -207,11 +160,10 @@ function getStrayServerInstance_LINUX() { }); ss.on('close', (code) => { - if (code !== 0) { + if (code !== 0) return reject(`ss command failed with code ${code}`); - } - const port = getConfigAttribute("mc_port"); // assumed defined + const port = configUtils.getConfigAttribute("mc_port"); // assumed defined const lines = ssOutput.split('\n'); for (const line of lines) { @@ -219,9 +171,8 @@ function getStrayServerInstance_LINUX() { const pidMatch = line.match(/pid=(\d+)/); if (pidMatch) { const pid = pidMatch[1]; - if (javaPIDs.includes(pid)) { + if (javaPIDs.includes(pid)) return resolve(parseInt(pid, 10)); - } } } } @@ -232,10 +183,15 @@ function getStrayServerInstance_LINUX() { }); } -async function killStrayServerInstance_LINUX() { +async function killStrayServerInstance() { try { - const strayServerPID = await getStrayServerInstance_LINUX(); - const command = `kill -9 ${strayServerPID}`; + if (configUtils.getConfigAttribute("os") == "Linux") { + const strayServerPID = await getStrayServerInstance_LINUX(); + const command = `kill -9 ${strayServerPID}`; + } else { + const strayServerPID = await getStrayServerInstance_WINDOWS(); + const command = `taskkill /PID ${strayServerPID} /F`; + } exec(command, (error, stdout, stderr) => { if (error) { @@ -250,36 +206,38 @@ async function killStrayServerInstance_LINUX() { }); } catch (error) { console.error(error); + } finally { + serverStatus = consts.serverStatus.OFFLINE; + deleteServerOutput(); } } - - -function validateMemory(){ +function validateMemory() { try { - const launchConfig = require("../server-config.json"); const availableMemory = Math.floor(freemem() / 1048576); - if(availableMemory > parseInt(launchConfig["memory"].replace("M",""))){ - + if (availableMemory > parseInt(launchConfig["memory"].replace("M",""))) { console.log(`${launchConfig["memory"]} Available for use!`); + return true; - }else{ + } else { console.log(`${launchConfig["memory"]} not available for use!`); + return false; - } } catch (error) { console.error(error); return false; } } + async function startServer() { - if(!validateMemory()){ + if (!validateMemory()) throw new Error("Not enough memory for server to run"); - } + const command = 'java'; - const args = ['-Xmx1024M', '-Xms1024M', '-jar', consts.serverName, 'nogui']; + const mem = configUtils.getConfigJSON()["memory"]; + const args = [`-Xmx${mem}M`, `-Xms${Number(mem) / 2}M`, '-jar', consts.serverName, 'nogui']; serverProcess = spawn(command, args, { cwd: consts.serverDirectory, // Set the working directory @@ -303,9 +261,8 @@ async function startServer() { } async function startServerWithScript() { - if(!validateMemory()){ + if (!validateMemory()) throw new Error("Not enough memory for server to run"); - } serverProcess = spawn('sh', ['start.sh'], { cwd: consts.serverDirectory, @@ -313,6 +270,7 @@ async function startServerWithScript() { }); fs.writeFileSync(consts.serverLogsFilePath, ''); + serverProcess.stdout.on('data', (data) => { console.log(`stdout: ${data}`); fs.appendFileSync(consts.serverLogsFilePath, data); @@ -327,17 +285,18 @@ async function startServerWithScript() { }); } - async function doesServerJarAlreadyExist() { return fs.existsSync("../server/server.jar"); } +function getServerProcess() { + return serverProcess; +} + module.exports = { isServerOn, getStrayServerInstance_WINDOWS, getStrayServerInstance_LINUX, - killStrayServerInstance_WINDOWS, - killStrayServerInstance_LINUX, startServer, isServerStarting, doesServerJarAlreadyExist, @@ -348,4 +307,6 @@ module.exports = { isEULAsigned, startServerWithScript, serverStatus, + deleteServerOutput, + getServerProcess }; \ No newline at end of file diff --git a/api/starter.js b/api/starter.js index c93f979..9f95191 100644 --- a/api/starter.js +++ b/api/starter.js @@ -1,6 +1,6 @@ -const networkingUtils = require('./utils/networkingUtils'); -const apiAccessUtils = require('./utils/apiAccessUtils'); -const configUtils = require('./utils/configUtils') +const networkingUtils = require('./utils/networking.util'); +const apiAccessUtils = require('./utils/access.util'); +const configUtils = require('./utils/config.util') const fs = require('fs'); const consts = require('./consts'); diff --git a/api/utils/apiAccessUtils.js b/api/utils/access.util.js similarity index 100% rename from api/utils/apiAccessUtils.js rename to api/utils/access.util.js diff --git a/api/utils/config.util.js b/api/utils/config.util.js new file mode 100644 index 0000000..a1c460b --- /dev/null +++ b/api/utils/config.util.js @@ -0,0 +1,66 @@ +const fs = require('fs'); +const os = require('os'); +const { configFilePath } = require("../consts"); + +const defaultConfig = { + "os": os.type(), + "memory": "1024M", + "platform": "vanilla", + "version": "1.21.4", + "start_with_script": false, + "mc_port": 25565, + "api_port": 3001, + "debug": false +}; + +function doesConfigExist() { + return fs.existsSync("server-config.json"); +} + +function getConfigAttribute(attributeName) { + try { + const jsonConfig = getConfigJSON(); + return jsonConfig[attributeName]; + } catch (error) { + return defaultConfig[attributeName]; + } +} + +function getConfigJSON() { + const config = fs.readFileSync(configFilePath, { encoding: 'utf8', flag: 'r' }); + return JSON.parse(config); +} + +function updateConfigAttribute(name, value) { + try { + var config = getConfigJSON(); + config[name] = value; + fs.writeFileSync(configFilePath, JSON.stringify(config, null, 4)); + } catch (error) { + throw new Error(`Failed to update config attribute: ${error.message}`); + } +} + +function updateMemoryAllocated(valueMB) { + const freeMemoryMB = Math.floor(os.freemem() / (1024 * 1024)); + const totalMemoryMB = Math.floor(os.totalmem() / (1024 * 1024)); + + if (valueMB > freeMemoryMB * 0.98) + throw new Error(`Not enough free memory! Only ${freeMemoryMB} MB free out of ${totalMemoryMB} MB`); + + updateConfigAttribute("memory", valueMB); +} + +function generateConfigFile() { + const jsonConfig = JSON.stringify(defaultConfig, null, 4); + fs.writeFileSync("./server-config.json", jsonConfig); +} + +module.exports = { + generateConfigFile, + doesConfigExist, + getConfigAttribute, + updateConfigAttribute, + getConfigJSON, + updateMemoryAllocated, +} \ No newline at end of file diff --git a/api/utils/configUtils.js b/api/utils/configUtils.js deleted file mode 100644 index 2d31df0..0000000 --- a/api/utils/configUtils.js +++ /dev/null @@ -1,83 +0,0 @@ -const fs = require('fs'); -const os = require('os'); -const { configFilePath } = require("../consts") - -const defaultConfig = { - "os": os.type(), - "memory": "1024M", - "platform": "vanilla", - "version": "1.21.4", - "mc_port": 25565, - "api_port": 3001, - "debug": false - -} - -function doesConfigExist(){ - return fs.existsSync("server-config.json") -} - -function getConfigAttribute(attributeName){ - try{ - const jsonConfig = getConfigJSON(); - return jsonConfig[attributeName]; - } catch (error) { - return defaultConfig[attributeName]; - } - -} - -function getConfigJSON(){ - const config = fs.readFileSync(configFilePath, { encoding: 'utf8', flag: 'r' }); - return JSON.parse(config); - -} - -function updateConfigAttribute(name, value){ - try { - var config = getConfigJSON(); - config[name] = value; - fs.writeFileSync(configFilePath, JSON.stringify(config, null, 4)); - } catch (error) { - throw new Error(`Failed to update config attribute: ${error.message}`); - } -} - -function updateMemoryAllocated(value){ - const freeMemoryMB = Math.floor(os.freemem()/0.000001); - if(value > freeMemoryMB * 0.98){ - throw new Error(`Not enough free memory! only ${freeMemoryMB} free of ${Math.floor(os.totalmem()/0.000001)}`); - } - updateConfigAttribute("memory", value); -} - -function generateConfigFile(OS=os.type(), - memory="1024M", - platform="vanilla", - version="1.21.4", - mc_port=25565, - api_port=3001, - debug=false, - ){ - let config = { - os: defaultConfig.os, - memory: defaultConfig.memory, - platform: defaultConfig.platform, - version: defaultConfig.version, - mc_port: defaultConfig.mc_port, - api_port: defaultConfig.api_port, - debug: defaultConfig.debug - }; - const jsonConfig = JSON.stringify(config, null, 4); - fs.writeFileSync("./server-config.json", jsonConfig); -} - - -module.exports = { - generateConfigFile, - doesConfigExist, - getConfigAttribute, - updateConfigAttribute, - getConfigJSON, - updateMemoryAllocated, -} \ No newline at end of file diff --git a/api/utils/jsonFilesUtils.js b/api/utils/files.util.js similarity index 100% rename from api/utils/jsonFilesUtils.js rename to api/utils/files.util.js diff --git a/api/utils/installations.util.js b/api/utils/installations.util.js new file mode 100644 index 0000000..69e6b8e --- /dev/null +++ b/api/utils/installations.util.js @@ -0,0 +1,23 @@ +const fs = require('fs'); +const consts = require("../consts"); + +async function writeDownloadedFile(response, version, platform) { + try { + if (response.ok) { + console.log(`Downloading ${platform} server.jar for version ${version}. STATUS: ${response.status}`); + const fileName = `${consts.serverDirectory}/${consts.serverName}`; + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + fs.writeFileSync(fileName, buffer); + console.log(`Successfully downloaded ${platform} ${fileName}!`); + } else { + throw new Error(`Failed to download ${platform} for version ${version}. STATUS: ${response.status}`); + } + } catch(error) { + console.error(error); + } +} + +module.exports = { + writeDownloadedFile, +} \ No newline at end of file diff --git a/api/utils/installationsUtils.js b/api/utils/installationsUtils.js deleted file mode 100644 index 8dc9093..0000000 --- a/api/utils/installationsUtils.js +++ /dev/null @@ -1,96 +0,0 @@ - -const fs = require('fs'); -const consts = require("../consts"); -const urlFetcher = require("./platformURLFetcherUtil"); - -async function downloadRouter(platform, version) { - switch(platform){ - case "vanilla": - await downloadVanilla(version); - break; - case "paper": - await downloadPaper(version); - break; - case "fabric": - await downloadFabric(version); - break; - case "forge": - await downloadForge(version); - break; - case _: - throw new Error(`Invalid platform --> ${platform}`) - - } -} - - - -async function downloadVanilla(version) { - try { - const response = await fetch( - await urlFetcher.fetchVanillaURL(version) - ); - await writeDownloadedFile(response, version, "VANILLA"); - - } catch (error) { - console.error(error); - } -} - -async function downloadForge(version) { - try { - const response = await fetch( - await urlFetcher.fetchForgeURL(version) - ); - await writeDownloadedFile(response, version, "FORGE"); - - } catch (error) { - console.error(error); - } -} - -async function downloadPaper(version) { - try { - const response = await fetch( - await urlFetcher.fetchPaperURL(version) - ); - await writeDownloadedFile(response, version, "PAPER"); - - } catch (error) { - console.error(error); - } -} - -async function downloadFabric(version) { - try { - const response = await fetch( - await urlFetcher.fetchFabricURL(version) - ); - await writeDownloadedFile(response, version, "FABRIC"); - - } catch (error) { - console.error(error); - } -} - - -async function writeDownloadedFile(response, version, platform) { - try{ - if (response.ok) { - console.log(`Downloading ${platform} server.jar for version ${version}. STATUS: ${response.status}`); - const fileName = `${consts.serverDirectory}/${consts.serverName}`; - const arrayBuffer = await response.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - fs.writeFileSync(fileName, buffer); - console.log(`Successfully downloaded ${platform} ${fileName}!`); - } else { - throw new Error(`Failed to download ${platform} for version ${version}. STATUS: ${response.status}`); - } - } catch(error){ - console.error(error); - } -} - -module.exports = { - downloadRouter, -} \ No newline at end of file diff --git a/api/utils/networkingUtils.js b/api/utils/networking.util.js similarity index 98% rename from api/utils/networkingUtils.js rename to api/utils/networking.util.js index c56f2f1..a414f00 100644 --- a/api/utils/networkingUtils.js +++ b/api/utils/networking.util.js @@ -2,7 +2,7 @@ const consts = require("../consts"); const { exec } = require('child_process'); const util = require('util'); const path = require('path'); -const configUtils = require('./configUtils') +const configUtils = require('./config.util') diff --git a/api/utils/platformURLFetcherUtil.js b/api/utils/url_fetcher.util.js similarity index 100% rename from api/utils/platformURLFetcherUtil.js rename to api/utils/url_fetcher.util.js