From a30eff761de00e99e3a3bac86071eb86f7758e42 Mon Sep 17 00:00:00 2001 From: DEVUCP Date: Wed, 25 Jun 2025 17:58:22 +0300 Subject: [PATCH 01/18] fix(api): Remove hardcoded use of starting with script, Add config option for starting with script --- api/routes/serverRoutes.js | 8 +++++++- api/utils/configUtils.js | 3 +++ api/utils/serverUtils.js | 12 +++++++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/api/routes/serverRoutes.js b/api/routes/serverRoutes.js index 809b0a7..b85d01c 100644 --- a/api/routes/serverRoutes.js +++ b/api/routes/serverRoutes.js @@ -2,6 +2,7 @@ const express = require('express'); const router = express.Router(); const serverUtils = require('../utils/serverUtils'); const { Sema } = require('async-sema'); +const configUtils = require('../utils/configUtils'); const consoleSema = new Sema(1); @@ -81,8 +82,13 @@ router.put('/start', async (req, res) => { } serverUtils.serverStatus = 2; - await serverUtils.startServerWithScript(); + if( configUtils.getConfigAttribute("start_with_script") ){ + await serverUtils.startServerWithScript(); + }else{ + await serverUtils.startServer(); + } res.send('Server started.'); + } catch (error) { serverUtils.serverStatus = 0; res.status(500).send(`Error starting server: ${error}`); diff --git a/api/utils/configUtils.js b/api/utils/configUtils.js index 2d31df0..3731897 100644 --- a/api/utils/configUtils.js +++ b/api/utils/configUtils.js @@ -7,6 +7,7 @@ const defaultConfig = { "memory": "1024M", "platform": "vanilla", "version": "1.21.4", + "start_with_script": false, "mc_port": 25565, "api_port": 3001, "debug": false @@ -55,6 +56,7 @@ function generateConfigFile(OS=os.type(), memory="1024M", platform="vanilla", version="1.21.4", + start_with_script=false, mc_port=25565, api_port=3001, debug=false, @@ -64,6 +66,7 @@ function generateConfigFile(OS=os.type(), memory: defaultConfig.memory, platform: defaultConfig.platform, version: defaultConfig.version, + start_with_script: defaultConfig.start_with_script, mc_port: defaultConfig.mc_port, api_port: defaultConfig.api_port, debug: defaultConfig.debug diff --git a/api/utils/serverUtils.js b/api/utils/serverUtils.js index 3f556d1..fc3647a 100644 --- a/api/utils/serverUtils.js +++ b/api/utils/serverUtils.js @@ -3,6 +3,7 @@ const fs = require('fs'); const consts = require("../consts"); const {freemem} = require('os'); const {getConfigAttribute} = require("./configUtils"); +const path = require('path'); let serverProcess = null; let serverStatus = 0; @@ -306,7 +307,11 @@ async function startServerWithScript() { if(!validateMemory()){ throw new Error("Not enough memory for server to run"); } - + try{ + if (!fs.existsSync(path.join(consts.serverDirectory, 'start.sh'))) { + throw new Error("start.sh script not found in server directory\n If you don't intend on using a script, set start_server_with_script to false in server-config.json"); + } + serverProcess = spawn('sh', ['start.sh'], { cwd: consts.serverDirectory, stdio: ['pipe', 'pipe', 'pipe'], @@ -325,6 +330,11 @@ async function startServerWithScript() { serverProcess.on('close', (code) => { console.log(`Server process exited with code ${code}`); }); + + + }catch(error){ + throw new Error(error); + } } From fa00d9a3aee0c6e7aa508f52ad6f6118da41ef41 Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Wed, 25 Jun 2025 18:48:26 +0300 Subject: [PATCH 02/18] refactor(api): Partial refactor on `startServer` and `runCommand` --- api/controllers/server.controller.js | 54 +++++ api/routes/serverRoutes.js | 43 +--- api/services/server.service.js | 347 +++++++++++++++++++++++++++ 3 files changed, 404 insertions(+), 40 deletions(-) create mode 100644 api/controllers/server.controller.js create mode 100644 api/services/server.service.js diff --git a/api/controllers/server.controller.js b/api/controllers/server.controller.js new file mode 100644 index 0000000..cac9777 --- /dev/null +++ b/api/controllers/server.controller.js @@ -0,0 +1,54 @@ +const serverService = require('../services/server.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(); + } +} + +async function startServer(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; + + if( configUtils.getConfigAttribute("start_with_script") ){ + await serverUtils.startServerWithScript(); + }else{ + await serverUtils.startServer(); + } + 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") + } +} + +module.exports = { + runCommand, + startServer +} \ No newline at end of file diff --git a/api/routes/serverRoutes.js b/api/routes/serverRoutes.js index b85d01c..fee4d24 100644 --- a/api/routes/serverRoutes.js +++ b/api/routes/serverRoutes.js @@ -1,23 +1,13 @@ const express = require('express'); const router = express.Router(); const serverUtils = require('../utils/serverUtils'); +const { runCommand, startServer } = require('../controllers/server.controller'); const { Sema } = require('async-sema'); const configUtils = require('../utils/configUtils'); 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.put('/console/run/:command', runCommand); router.get('/console-text', async (req, res) =>{ try { @@ -69,34 +59,7 @@ router.get('/check-server', async (req, res) => { 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; - - if( configUtils.getConfigAttribute("start_with_script") ){ - await serverUtils.startServerWithScript(); - }else{ - await serverUtils.startServer(); - } - 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") - } -}); +router.put('/start', startServer); // Route to stop the server router.put('/stop', async (req, res) => { diff --git a/api/services/server.service.js b/api/services/server.service.js new file mode 100644 index 0000000..00cfab2 --- /dev/null +++ b/api/services/server.service.js @@ -0,0 +1,347 @@ +const { spawn, exec } = require('child_process'); +const fs = require('fs'); +const consts = require("../consts"); +const {freemem} = require('os'); +const {getConfigAttribute} = require("../utils/configUtils"); + +let serverProcess = null; +let serverStatus = 0; + +async function isServerOn() { + try { + const serverPID = getConfigAttribute("os") != "Linux" ? await getStrayServerInstance_WINDOWS() : await getStrayServerInstance_LINUX(); + return !!serverPID || serverStatus == 1; + } catch (error) { + return false; + } +} +function deleteServerOutput(){ + try { + fs.rmSync(consts.serverLogsFilePath, { force: true }); + } catch (error) { + console.error(error); + } +} + +function isServerStarting(){ + + const serverLogs = getServerlogs(); + if(serverLogs == null || serverLogs.includes("All dimensions are saved")){ + return 0; + } + if(serverLogs.includes("Starting") && !serverLogs.includes("Done")){ + serverStatus = 2; + return serverStatus; + }else if(serverLogs.includes("Done")){ + serverStatus = 1; + return serverStatus; + } + else{ + return 0; + } +} + +function isEULAsigned() { + if (fs.existsSync(consts.eulaFilePath)){ + if (fs.readFileSync(consts.eulaFilePath, 'utf8').includes('eula=true')) { + return true; + } + else + return false; + } + else + return false; +} + +function signEULA() { + fs.writeFileSync(consts.eulaFilePath, 'eula=true'); +} + +function getServerlogs() { + try { + 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.") + } + serverProcess.stdin.write(`${command}\n`); + } 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']); + + let output = ''; + tasklist.stdout.on('data', (data) => { + output += data.toString(); + }); + + tasklist.stderr.on('data', (data) => { + reject(`Error in tasklist: ${data.toString()}`); + }); + + tasklist.on('close', (code) => { + if (code !== 0) { + reject(`tasklist command failed with code ${code}`); + } else if (output.includes('java.exe')) { + const netstat = spawn('netstat', ['-ano']); + + let netstatOutput = ''; + netstat.stdout.on('data', (data) => { + netstatOutput += data.toString(); + }); + + netstat.stderr.on('data', (data) => { + reject(`Error in netstat: ${data.toString()}`); + }); + + netstat.on('close', (code) => { + if (code !== 0) { + reject(`netstat command failed with code ${code}`); + } else { + + const port = getConfigAttribute("port"); + const regex = new RegExp(`TCP\\s+.*:${port}\\s+.*\\s+LISTENING\\s+(\\d+)`, 'i'); + const match = netstatOutput.match(regex); + + if (match) { + resolve(parseInt(match[1])); + } 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 + const ps = spawn('ps', ['-C', 'java', '-o', 'pid=']); + + let output = ''; + ps.stdout.on('data', (data) => { + output += data.toString(); + }); + + ps.stderr.on('data', (data) => { + reject(`Error in ps: ${data.toString()}`); + }); + + ps.on('close', (code) => { + 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) { + return reject('No Java processes found.'); + } + + // Step 2: Check for listeners on the target port + const ss = spawn('ss', ['-tlnp']); + let ssOutput = ''; + + ss.stdout.on('data', (data) => { + ssOutput += data.toString(); + }); + + ss.stderr.on('data', (data) => { + reject(`Error in ss: ${data.toString()}`); + }); + + ss.on('close', (code) => { + if (code !== 0) { + return reject(`ss command failed with code ${code}`); + } + + const port = getConfigAttribute("mc_port"); // assumed defined + const lines = ssOutput.split('\n'); + + for (const line of lines) { + if (line.includes(`:${port}`) && line.includes('LISTEN') && line.includes('pid=')) { + const pidMatch = line.match(/pid=(\d+)/); + if (pidMatch) { + const pid = pidMatch[1]; + if (javaPIDs.includes(pid)) { + return resolve(parseInt(pid, 10)); + } + } + } + } + + reject(`No Java process found listening on port ${port}`); + }); + }); + }); +} + +async function killStrayServerInstance_LINUX() { + try { + const strayServerPID = await getStrayServerInstance_LINUX(); + const command = `kill -9 ${strayServerPID}`; + + 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 validateMemory(){ + try { + + const launchConfig = require("../server-config.json"); + const availableMemory = Math.floor(freemem() / 1048576); + if(availableMemory > parseInt(launchConfig["memory"].replace("M",""))){ + + console.log(`${launchConfig["memory"]} Available for use!`); + return true; + }else{ + console.log(`${launchConfig["memory"]} not available for use!`); + return false; + + } + } catch (error) { + console.error(error); + return false; + } +} + +async function startServer() { + if(!validateMemory()){ + throw new Error("Not enough memory for server to run"); + } + const command = 'java'; + const args = ['-Xmx1024M', '-Xms1024M', '-jar', consts.serverName, 'nogui']; + + serverProcess = spawn(command, args, { + cwd: consts.serverDirectory, // Set the working directory + stdio: ['pipe', 'pipe', 'pipe'], // Use pipes for stdin, stdout, and stderr + }); + + fs.writeFileSync(consts.serverLogsFilePath, ''); + + serverProcess.stdout.on('data', (data) => { + console.log(`stdout: ${data}`); + fs.appendFileSync(consts.serverLogsFilePath, data); + }); + + serverProcess.stderr.on('data', (data) => { + console.error(`stderr: ${data}`); + }); + + serverProcess.on('close', (code) => { + console.log(`Server process exited with code ${code}`); + }); +} + +async function startServerWithScript() { + if(!validateMemory()){ + throw new Error("Not enough memory for server to run"); + } + + serverProcess = spawn('sh', ['start.sh'], { + cwd: consts.serverDirectory, + stdio: ['pipe', 'pipe', 'pipe'], + }); + + fs.writeFileSync(consts.serverLogsFilePath, ''); + serverProcess.stdout.on('data', (data) => { + console.log(`stdout: ${data}`); + fs.appendFileSync(consts.serverLogsFilePath, data); + }); + + serverProcess.stderr.on('data', (data) => { + console.error(`stderr: ${data}`); + }); + + serverProcess.on('close', (code) => { + console.log(`Server process exited with code ${code}`); + }); +} + +async function doesServerJarAlreadyExist() { + return fs.existsSync("../server/server.jar"); +} + +module.exports = { + isServerOn, + getStrayServerInstance_WINDOWS, + getStrayServerInstance_LINUX, + killStrayServerInstance_WINDOWS, + killStrayServerInstance_LINUX, + startServer, + isServerStarting, + doesServerJarAlreadyExist, + getServerlogs, + runMCCommand, + killStrayServerInstance, + signEULA, + isEULAsigned, + startServerWithScript, + serverStatus, +}; \ No newline at end of file From d32641823428df3cbea00a8b7bee4543237d4634 Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Wed, 25 Jun 2025 23:46:22 +0300 Subject: [PATCH 03/18] refactor(api): Full server routes refactor --- api/consts.js | 6 + api/controllers/server.controller.js | 102 +++++++- api/routes/serverRoutes.js | 99 ++------ api/services/server.service.js | 156 +++++------- api/utils/serverUtils.js | 361 --------------------------- 5 files changed, 169 insertions(+), 555 deletions(-) delete mode 100644 api/utils/serverUtils.js 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/server.controller.js b/api/controllers/server.controller.js index cac9777..b4526d0 100644 --- a/api/controllers/server.controller.js +++ b/api/controllers/server.controller.js @@ -1,8 +1,9 @@ const serverService = require('../services/server.service'); +const configUtils = require('../utils/configUtils'); + const { Sema } = require('async-sema'); const consoleSema = new Sema(1); - async function runCommand(req, res) { await consoleSema.acquire(); @@ -11,7 +12,7 @@ async function runCommand(req, res) { res.status(200).send("done"); } catch(error) { - console.error(error) + console.error(error); res.status(500).send("Error: " + error.message); } finally { @@ -19,36 +20,115 @@ async function runCommand(req, res) { } } +const startStopSema = new Sema(1); async function startServer(req, res) { await startStopSema.acquire(); try { - if (await serverUtils.isServerOn()) { + if (await serverService.isServerOn()) { res.status(400).send('Server is already running.'); return; } - if (!serverUtils.isEULAsigned()){ + if (!serverService.isEULAsigned()){ res.status(400).send('EULA must be signed.'); return; } - serverUtils.serverStatus = 2; + serverService.serverStatus = 2; if( configUtils.getConfigAttribute("start_with_script") ){ - await serverUtils.startServerWithScript(); + await serverService.startServerWithScript(); }else{ - await serverUtils.startServer(); + await serverService.startServer(); } res.send('Server started.'); } catch (error) { - serverUtils.serverStatus = 0; + serverService.serverStatus = 0; res.status(500).send(`Error starting server: ${error}`); } finally { - startStopSema.release() - // console.log("startstop sema RELEASED") + 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; + } + + 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 + startServer, + getServerConsoleOutput, + checkExist, + checkServerStatus, + stopServer, + signEULA } \ No newline at end of file diff --git a/api/routes/serverRoutes.js b/api/routes/serverRoutes.js index fee4d24..c2fba49 100644 --- a/api/routes/serverRoutes.js +++ b/api/routes/serverRoutes.js @@ -1,102 +1,31 @@ const express = require('express'); const router = express.Router(); -const serverUtils = require('../utils/serverUtils'); -const { runCommand, startServer } = require('../controllers/server.controller'); -const { Sema } = require('async-sema'); -const configUtils = require('../utils/configUtils'); -const consoleSema = new Sema(1); +const { + runCommand, + startServer, + getServerConsoleOutput, + checkExist, + checkServerStatus, + stopServer, + signEULA +} = require('../controllers/server.controller'); router.put('/console/run/:command', runCommand); -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('/console-text', getServerConsoleOutput); - - -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"); - } -}) +router.get('/check-exist', checkExist) // 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); +router.get('/check-server', checkServerStatus); // Route to start the server router.put('/start', startServer); // 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('/stop', stopServer); -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}`); - } -}); +router.put('/sign-eula', signEULA); module.exports = router; \ No newline at end of file diff --git a/api/services/server.service.js b/api/services/server.service.js index 00cfab2..a372ee0 100644 --- a/api/services/server.service.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("../utils/configUtils"); +const { freemem } = require('os'); +const { getConfigAttribute } = require("../utils/configUtils"); 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; + 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,43 +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']); @@ -110,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 = ''; @@ -125,49 +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 = 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 @@ -183,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']); @@ -205,9 +160,8 @@ 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 lines = ssOutput.split('\n'); @@ -217,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)); - } } } } @@ -230,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 (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) { @@ -248,22 +206,25 @@ 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); @@ -272,9 +233,9 @@ function validateMemory(){ } 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']; @@ -300,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, @@ -310,6 +270,7 @@ async function startServerWithScript() { }); fs.writeFileSync(consts.serverLogsFilePath, ''); + serverProcess.stdout.on('data', (data) => { console.log(`stdout: ${data}`); fs.appendFileSync(consts.serverLogsFilePath, data); @@ -332,8 +293,6 @@ module.exports = { isServerOn, getStrayServerInstance_WINDOWS, getStrayServerInstance_LINUX, - killStrayServerInstance_WINDOWS, - killStrayServerInstance_LINUX, startServer, isServerStarting, doesServerJarAlreadyExist, @@ -344,4 +303,5 @@ module.exports = { isEULAsigned, startServerWithScript, serverStatus, + deleteServerOutput }; \ No newline at end of file diff --git a/api/utils/serverUtils.js b/api/utils/serverUtils.js deleted file mode 100644 index fc3647a..0000000 --- a/api/utils/serverUtils.js +++ /dev/null @@ -1,361 +0,0 @@ -const { spawn, exec } = require('child_process'); -const fs = require('fs'); -const consts = require("../consts"); -const {freemem} = require('os'); -const {getConfigAttribute} = require("./configUtils"); -const path = require('path'); - -let serverProcess = null; -let serverStatus = 0; - -async function isServerOn() { - try { - const serverPID = getConfigAttribute("os") != "Linux" ? await getStrayServerInstance_WINDOWS() : await getStrayServerInstance_LINUX(); - return !!serverPID || serverStatus == 1; - } catch (error) { - return false; - } -} -function deleteServerOutput(){ - try { - fs.rmSync(consts.serverLogsFilePath, { force: true }); - } catch (error) { - console.error(error); - } -} - -function isServerStarting(){ - - const serverLogs = getServerlogs(); - if(serverLogs == null || serverLogs.includes("All dimensions are saved")){ - return 0; - } - if(serverLogs.includes("Starting") && !serverLogs.includes("Done")){ - serverStatus = 2; - return serverStatus; - }else if(serverLogs.includes("Done")){ - serverStatus = 1; - return serverStatus; - } - else{ - return 0; - } -} - -function isEULAsigned() { - if (fs.existsSync(consts.eulaFilePath)){ - if (fs.readFileSync(consts.eulaFilePath, 'utf8').includes('eula=true')) { - return true; - } - else - return false; - } - else - return false; -} - -function signEULA() { - fs.writeFileSync(consts.eulaFilePath, 'eula=true'); -} - -function getServerlogs() { - try { - 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.") - } - serverProcess.stdin.write(`${command}\n`); - } 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']); - - let output = ''; - tasklist.stdout.on('data', (data) => { - output += data.toString(); - }); - - tasklist.stderr.on('data', (data) => { - reject(`Error in tasklist: ${data.toString()}`); - }); - - tasklist.on('close', (code) => { - if (code !== 0) { - reject(`tasklist command failed with code ${code}`); - } else if (output.includes('java.exe')) { - const netstat = spawn('netstat', ['-ano']); - - let netstatOutput = ''; - netstat.stdout.on('data', (data) => { - netstatOutput += data.toString(); - }); - - netstat.stderr.on('data', (data) => { - reject(`Error in netstat: ${data.toString()}`); - }); - - netstat.on('close', (code) => { - if (code !== 0) { - reject(`netstat command failed with code ${code}`); - } else { - - const port = getConfigAttribute("port"); - const regex = new RegExp(`TCP\\s+.*:${port}\\s+.*\\s+LISTENING\\s+(\\d+)`, 'i'); - const match = netstatOutput.match(regex); - - if (match) { - resolve(parseInt(match[1])); - } 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 - const ps = spawn('ps', ['-C', 'java', '-o', 'pid=']); - - let output = ''; - ps.stdout.on('data', (data) => { - output += data.toString(); - }); - - ps.stderr.on('data', (data) => { - reject(`Error in ps: ${data.toString()}`); - }); - - ps.on('close', (code) => { - 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) { - return reject('No Java processes found.'); - } - - // Step 2: Check for listeners on the target port - const ss = spawn('ss', ['-tlnp']); - let ssOutput = ''; - - ss.stdout.on('data', (data) => { - ssOutput += data.toString(); - }); - - ss.stderr.on('data', (data) => { - reject(`Error in ss: ${data.toString()}`); - }); - - ss.on('close', (code) => { - if (code !== 0) { - return reject(`ss command failed with code ${code}`); - } - - const port = getConfigAttribute("mc_port"); // assumed defined - const lines = ssOutput.split('\n'); - - for (const line of lines) { - if (line.includes(`:${port}`) && line.includes('LISTEN') && line.includes('pid=')) { - const pidMatch = line.match(/pid=(\d+)/); - if (pidMatch) { - const pid = pidMatch[1]; - if (javaPIDs.includes(pid)) { - return resolve(parseInt(pid, 10)); - } - } - } - } - - reject(`No Java process found listening on port ${port}`); - }); - }); - }); -} - -async function killStrayServerInstance_LINUX() { - try { - const strayServerPID = await getStrayServerInstance_LINUX(); - const command = `kill -9 ${strayServerPID}`; - - 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 validateMemory(){ - try { - - const launchConfig = require("../server-config.json"); - const availableMemory = Math.floor(freemem() / 1048576); - if(availableMemory > parseInt(launchConfig["memory"].replace("M",""))){ - - console.log(`${launchConfig["memory"]} Available for use!`); - return true; - }else{ - console.log(`${launchConfig["memory"]} not available for use!`); - return false; - - } - } catch (error) { - console.error(error); - return false; - } -} -async function startServer() { - if(!validateMemory()){ - throw new Error("Not enough memory for server to run"); - } - const command = 'java'; - const args = ['-Xmx1024M', '-Xms1024M', '-jar', consts.serverName, 'nogui']; - - serverProcess = spawn(command, args, { - cwd: consts.serverDirectory, // Set the working directory - stdio: ['pipe', 'pipe', 'pipe'], // Use pipes for stdin, stdout, and stderr - }); - - fs.writeFileSync(consts.serverLogsFilePath, ''); - - serverProcess.stdout.on('data', (data) => { - console.log(`stdout: ${data}`); - fs.appendFileSync(consts.serverLogsFilePath, data); - }); - - serverProcess.stderr.on('data', (data) => { - console.error(`stderr: ${data}`); - }); - - serverProcess.on('close', (code) => { - console.log(`Server process exited with code ${code}`); - }); -} - -async function startServerWithScript() { - if(!validateMemory()){ - throw new Error("Not enough memory for server to run"); - } - try{ - if (!fs.existsSync(path.join(consts.serverDirectory, 'start.sh'))) { - throw new Error("start.sh script not found in server directory\n If you don't intend on using a script, set start_server_with_script to false in server-config.json"); - } - - serverProcess = spawn('sh', ['start.sh'], { - cwd: consts.serverDirectory, - stdio: ['pipe', 'pipe', 'pipe'], - }); - - fs.writeFileSync(consts.serverLogsFilePath, ''); - serverProcess.stdout.on('data', (data) => { - console.log(`stdout: ${data}`); - fs.appendFileSync(consts.serverLogsFilePath, data); - }); - - serverProcess.stderr.on('data', (data) => { - console.error(`stderr: ${data}`); - }); - - serverProcess.on('close', (code) => { - console.log(`Server process exited with code ${code}`); - }); - - - }catch(error){ - throw new Error(error); - } -} - - -async function doesServerJarAlreadyExist() { - return fs.existsSync("../server/server.jar"); -} - -module.exports = { - isServerOn, - getStrayServerInstance_WINDOWS, - getStrayServerInstance_LINUX, - killStrayServerInstance_WINDOWS, - killStrayServerInstance_LINUX, - startServer, - isServerStarting, - doesServerJarAlreadyExist, - getServerlogs, - runMCCommand, - killStrayServerInstance, - signEULA, - isEULAsigned, - startServerWithScript, - serverStatus, -}; \ No newline at end of file From 0e27a80875b5ee863075705c279b7de93437aa45 Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Thu, 26 Jun 2025 00:12:55 +0300 Subject: [PATCH 04/18] refactor(api): Full installations routes refactor --- api/controllers/installations.controller.js | 25 +++++++ api/routes/installationsRoutes.js | 25 +------ api/services/installations.service.js | 27 ++++++++ api/utils/installationsUtils.js | 75 +-------------------- 4 files changed, 55 insertions(+), 97 deletions(-) create mode 100644 api/controllers/installations.controller.js create mode 100644 api/services/installations.service.js diff --git a/api/controllers/installations.controller.js b/api/controllers/installations.controller.js new file mode 100644 index 0000000..65a84fa --- /dev/null +++ b/api/controllers/installations.controller.js @@ -0,0 +1,25 @@ +const installationsUtils = require('../utils/installationsUtils'); +const { updateConfigAttribute } = require('../utils/configUtils'); +const { Sema } = require('async-sema'); + +const downloadSema = new Sema(1); + +async function downloadServer(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 = { + downloadServer +} \ No newline at end of file diff --git a/api/routes/installationsRoutes.js b/api/routes/installationsRoutes.js index bf9351a..f510ade 100644 --- a/api/routes/installationsRoutes.js +++ b/api/routes/installationsRoutes.js @@ -1,29 +1,8 @@ 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(); - } -}); +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/services/installations.service.js b/api/services/installations.service.js new file mode 100644 index 0000000..31e508d --- /dev/null +++ b/api/services/installations.service.js @@ -0,0 +1,27 @@ +const urlFetcher = require("../utils/platformURLFetcherUtil"); +const { writeDownloadedFile } = require("../utils/installationsUtils"); + +async function downloadRouter(platform, version) { + let response; + try { + switch(platform) { + case "vanilla": + response = await fetch(await urlFetcher.fetchVanillaURL(version)); + case "paper": + response = await fetch(await urlFetcher.fetchPaperURL(version)); + case "fabric": + response = await fetch(await urlFetcher.fetchFabricURL(version)); + case "forge": + response = await fetch(await urlFetcher.fetchForgeURL(version)); + case _: + 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/installationsUtils.js b/api/utils/installationsUtils.js index 8dc9093..589eb2d 100644 --- a/api/utils/installationsUtils.js +++ b/api/utils/installationsUtils.js @@ -1,78 +1,5 @@ - 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{ @@ -92,5 +19,5 @@ async function writeDownloadedFile(response, version, platform) { } module.exports = { - downloadRouter, + writeDownloadedFile, } \ No newline at end of file From d1eb581e8c37feec174b5c5bbc6d37cc50b42e55 Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Thu, 26 Jun 2025 00:24:51 +0300 Subject: [PATCH 05/18] style(api): Clean up `configUtils` --- api/utils/configUtils.js | 56 +++++++++++++--------------------------- 1 file changed, 18 insertions(+), 38 deletions(-) diff --git a/api/utils/configUtils.js b/api/utils/configUtils.js index 3731897..a1c460b 100644 --- a/api/utils/configUtils.js +++ b/api/utils/configUtils.js @@ -1,6 +1,6 @@ const fs = require('fs'); const os = require('os'); -const { configFilePath } = require("../consts") +const { configFilePath } = require("../consts"); const defaultConfig = { "os": os.type(), @@ -11,30 +11,27 @@ const defaultConfig = { "mc_port": 25565, "api_port": 3001, "debug": false +}; +function doesConfigExist() { + return fs.existsSync("server-config.json"); } -function doesConfigExist(){ - return fs.existsSync("server-config.json") -} - -function getConfigAttribute(attributeName){ - try{ +function getConfigAttribute(attributeName) { + try { const jsonConfig = getConfigJSON(); return jsonConfig[attributeName]; } catch (error) { return defaultConfig[attributeName]; } - } -function getConfigJSON(){ +function getConfigJSON() { const config = fs.readFileSync(configFilePath, { encoding: 'utf8', flag: 'r' }); return JSON.parse(config); - } -function updateConfigAttribute(name, value){ +function updateConfigAttribute(name, value) { try { var config = getConfigJSON(); config[name] = value; @@ -44,38 +41,21 @@ function updateConfigAttribute(name, value){ } } -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 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(OS=os.type(), - memory="1024M", - platform="vanilla", - version="1.21.4", - start_with_script=false, - mc_port=25565, - api_port=3001, - debug=false, - ){ - let config = { - os: defaultConfig.os, - memory: defaultConfig.memory, - platform: defaultConfig.platform, - version: defaultConfig.version, - start_with_script: defaultConfig.start_with_script, - mc_port: defaultConfig.mc_port, - api_port: defaultConfig.api_port, - debug: defaultConfig.debug - }; - const jsonConfig = JSON.stringify(config, null, 4); +function generateConfigFile() { + const jsonConfig = JSON.stringify(defaultConfig, null, 4); fs.writeFileSync("./server-config.json", jsonConfig); } - module.exports = { generateConfigFile, doesConfigExist, From a51da60622bfef086eab5af61a057648104486b4 Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Thu, 26 Jun 2025 00:33:57 +0300 Subject: [PATCH 06/18] style(api): Clean up `cleaner` --- api/cleaner.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/api/cleaner.js b/api/cleaner.js index 9029e3e..99b8b0c 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 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); From 415f254c3898f284ddbb4cedc911c6f9c467cfce Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Thu, 26 Jun 2025 22:08:06 +0300 Subject: [PATCH 07/18] refactor(api): Full properties routes refactor --- api/controllers/properties.controller.js | 176 ++++++++++++++++ api/routes/propertiesRoutes.js | 188 +++--------------- .../properties.service.js} | 4 +- 3 files changed, 207 insertions(+), 161 deletions(-) create mode 100644 api/controllers/properties.controller.js rename api/{utils/propertiesUtils.js => services/properties.service.js} (98%) diff --git a/api/controllers/properties.controller.js b/api/controllers/properties.controller.js new file mode 100644 index 0000000..a051515 --- /dev/null +++ b/api/controllers/properties.controller.js @@ -0,0 +1,176 @@ +const propertiesService = require("../services/properties.service"); +const configUtils = require("../utils/configUtils"); +const jsonFilesUtils = require("../utils/jsonFilesUtils"); + +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 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 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, + playerCount, + serverConfig, + getWhitelist, + getOps, + getBannedPlayers, + modifyOperator, + modifyWhitelist, + modifyBanned, + modifyBannedIPs +} \ No newline at end of file diff --git a/api/routes/propertiesRoutes.js b/api/routes/propertiesRoutes.js index 8d33484..b9f77fd 100644 --- a/api/routes/propertiesRoutes.js +++ b/api/routes/propertiesRoutes.js @@ -1,179 +1,51 @@ 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); - +const propertiesServices = require('../services/properties.service'); + +const { + toggleProperty, + allocateRam, + playerCount, + serverConfig, + getWhitelist, + getOps, + getBannedPlayers, + modifyOperator, + modifyWhitelist, + modifyBanned, + modifyBannedIPs + } = require("../controllers/properties.controller"); router.get('/', async (req, res) => { - try{ - const properties = await propertiesUtils.getProperties(); + try { + const properties = await propertiesServices.getProperties(); res.status(200).contentType("json").send(properties); - }catch(error){ - console.error(error) + } catch(error) { + console.error(error); res.status(500).send("error.. " + error.message); } }) +router.put('/toggle/:property', toggleProperty); -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', allocateRam); +router.get('/player-count', playerCount); -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('/server-config.json', serverConfig); -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('/whitelist.json', getWhitelist); -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`); +router.get('/ops.json', getOps); - } else { - res.status(404).send(`Invalid operation ${req.params.operation}`); - } +router.get('/banned-players.json', getBannedPlayers); - }catch(error){ - console.error(error) - res.status(500).send("error.. " + error.message); - } -}) +router.put('/:operation/op/:playername', modifyOperator); -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} `); +router.put('/:operation/whitelist/:playername', modifyWhitelist); - } else if(req.params.operation === "remove") { - await jsonFilesUtils.modifyBannedPlayersJSON(req.params.playername, false); - res.status(200).send(`Pardoned ${req.params.playername}`); +router.put('/:operation/ban/:playername', modifyBanned); - } 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); - } -}) +router.put('/:operation/ban-ip/:ip', modifyBannedIPs); module.exports = router; \ 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..7337f1b 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/configUtils"); 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, From 0fc0a8c5820504d34e2d77997b18aebb283ef8ed Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Thu, 26 Jun 2025 22:17:33 +0300 Subject: [PATCH 08/18] refactor(api): Full `app` and `middleware` refactor --- api/app.js | 27 ++++++++++++++++++++ api/middleware/limiter.middleware.js | 14 ++++++++++ api/middleware/middleware.js | 36 -------------------------- api/server.js | 38 +--------------------------- 4 files changed, 42 insertions(+), 73 deletions(-) create mode 100644 api/app.js create mode 100644 api/middleware/limiter.middleware.js delete mode 100644 api/middleware/middleware.js diff --git a/api/app.js b/api/app.js new file mode 100644 index 0000000..c166b9c --- /dev/null +++ b/api/app.js @@ -0,0 +1,27 @@ +const express = require('express'); +const cors = require('cors'); +const { limiter } = require('./middleware/limiter.middleware'); + +const app = express(); + +const adminRoutes = require('./routes/adminRoutes'); +const serverRoutes = require('./routes/serverRoutes'); // Routes are separated +const propertiesRoute = require('./routes/propertiesRoutes'); // Routes are separated +const installationsRoutes = require('./routes/installationsRoutes'); // Routes are separated + +app.use(cors()); +app.use(limiter) +app.use(express.json()); + +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`); +}); + +module.exports = { + app +} \ 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/server.js b/api/server.js index 80b6958..b1aa4ab 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 { app } = require('./app'); 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 // 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}`); From 632528b25a1815554ab1f79228313e4052f944f7 Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Thu, 26 Jun 2025 22:19:24 +0300 Subject: [PATCH 09/18] refactor(api): Rename all routes to convention --- api/app.js | 8 ++++---- api/routes/{adminRoutes.js => admin.routes.js} | 0 .../{installationsRoutes.js => installations.routes.js} | 0 api/routes/{propertiesRoutes.js => properties.routes.js} | 0 api/routes/{serverRoutes.js => server.routes.js} | 0 5 files changed, 4 insertions(+), 4 deletions(-) rename api/routes/{adminRoutes.js => admin.routes.js} (100%) rename api/routes/{installationsRoutes.js => installations.routes.js} (100%) rename api/routes/{propertiesRoutes.js => properties.routes.js} (100%) rename api/routes/{serverRoutes.js => server.routes.js} (100%) diff --git a/api/app.js b/api/app.js index c166b9c..38579e8 100644 --- a/api/app.js +++ b/api/app.js @@ -4,10 +4,10 @@ const { limiter } = require('./middleware/limiter.middleware'); const app = express(); -const adminRoutes = require('./routes/adminRoutes'); -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 adminRoutes = require('./routes/admin.routes'); +const serverRoutes = require('./routes/server.routes'); // Routes are separated +const propertiesRoute = require('./routes/properties.routes'); // Routes are separated +const installationsRoutes = require('./routes/installations.routes'); // Routes are separated app.use(cors()); app.use(limiter) diff --git a/api/routes/adminRoutes.js b/api/routes/admin.routes.js similarity index 100% rename from api/routes/adminRoutes.js rename to api/routes/admin.routes.js diff --git a/api/routes/installationsRoutes.js b/api/routes/installations.routes.js similarity index 100% rename from api/routes/installationsRoutes.js rename to api/routes/installations.routes.js diff --git a/api/routes/propertiesRoutes.js b/api/routes/properties.routes.js similarity index 100% rename from api/routes/propertiesRoutes.js rename to api/routes/properties.routes.js diff --git a/api/routes/serverRoutes.js b/api/routes/server.routes.js similarity index 100% rename from api/routes/serverRoutes.js rename to api/routes/server.routes.js From b6d5830b79052dff598c97e56314e3fcf038f16d Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Sat, 28 Jun 2025 14:47:24 +0300 Subject: [PATCH 10/18] fix(api): General fixes `installations` --- api/controllers/installations.controller.js | 4 ++-- api/services/installations.service.js | 4 ++++ api/utils/installationsUtils.js | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/api/controllers/installations.controller.js b/api/controllers/installations.controller.js index 65a84fa..53a28e9 100644 --- a/api/controllers/installations.controller.js +++ b/api/controllers/installations.controller.js @@ -1,4 +1,4 @@ -const installationsUtils = require('../utils/installationsUtils'); +const installationsService = require('../services/installations.service'); const { updateConfigAttribute } = require('../utils/configUtils'); const { Sema } = require('async-sema'); @@ -8,7 +8,7 @@ async function downloadServer(req, res) { await downloadSema.acquire(); try { - await installationsUtils.downloadRouter(req.params.platform, req.params.version); + await installationsService.downloadRouter(req.params.platform, req.params.version); res.status(201).send('Downloaded Successfully'); updateConfigAttribute("platform", req.params.platform); diff --git a/api/services/installations.service.js b/api/services/installations.service.js index 31e508d..11028a2 100644 --- a/api/services/installations.service.js +++ b/api/services/installations.service.js @@ -7,12 +7,16 @@ async function downloadRouter(platform, version) { 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; case _: throw new Error(`Invalid platform --> ${platform}`) } diff --git a/api/utils/installationsUtils.js b/api/utils/installationsUtils.js index 589eb2d..69e6b8e 100644 --- a/api/utils/installationsUtils.js +++ b/api/utils/installationsUtils.js @@ -2,7 +2,7 @@ const fs = require('fs'); const consts = require("../consts"); async function writeDownloadedFile(response, version, platform) { - try{ + try { if (response.ok) { console.log(`Downloading ${platform} server.jar for version ${version}. STATUS: ${response.status}`); const fileName = `${consts.serverDirectory}/${consts.serverName}`; @@ -13,7 +13,7 @@ async function writeDownloadedFile(response, version, platform) { } else { throw new Error(`Failed to download ${platform} for version ${version}. STATUS: ${response.status}`); } - } catch(error){ + } catch(error) { console.error(error); } } From f39c6630ca0a0a2bb0bf6d4aa4e998b78d39f492 Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Sat, 28 Jun 2025 14:48:04 +0300 Subject: [PATCH 11/18] chore(api): Add `adm-zip` and `pidusage` packages --- api/package-lock.json | 25 ++++++++++++++++++++++++- api/package.json | 4 +++- 2 files changed, 27 insertions(+), 2 deletions(-) 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" } } From 5f4ddb5c17a22973f1e5e8849b63b792e7661081 Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Sat, 28 Jun 2025 14:51:14 +0300 Subject: [PATCH 12/18] feat(api): Create `info` route --- api/app.js | 8 +- api/controllers/info.controller.js | 90 ++++++++++++++++ api/controllers/properties.controller.js | 12 --- api/controllers/server.controller.js | 3 + api/routes/info.routes.js | 25 +++++ api/routes/properties.routes.js | 3 - api/services/info.service.js | 129 +++++++++++++++++++++++ api/services/server.service.js | 21 ++-- 8 files changed, 265 insertions(+), 26 deletions(-) create mode 100644 api/controllers/info.controller.js create mode 100644 api/routes/info.routes.js create mode 100644 api/services/info.service.js diff --git a/api/app.js b/api/app.js index 38579e8..82f0d44 100644 --- a/api/app.js +++ b/api/app.js @@ -5,9 +5,10 @@ const { limiter } = require('./middleware/limiter.middleware'); const app = express(); const adminRoutes = require('./routes/admin.routes'); -const serverRoutes = require('./routes/server.routes'); // Routes are separated -const propertiesRoute = require('./routes/properties.routes'); // Routes are separated -const installationsRoutes = require('./routes/installations.routes'); // Routes are separated +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) @@ -16,6 +17,7 @@ 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) => { diff --git a/api/controllers/info.controller.js b/api/controllers/info.controller.js new file mode 100644 index 0000000..636232e --- /dev/null +++ b/api/controllers/info.controller.js @@ -0,0 +1,90 @@ +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 { + if (infoService.getStartTime() === null) { + res.status(200).send({ uptime: "0s" }); + return; + } + + const ms = Date.now() - infoService.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`); + + res.status(200).send({ uptime: formatted.join(" ") }); + } 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); + } +} + +module.exports = { + playerCount, + getUpTime, + getMemoryUsage, + getWorldSize, + getVersion, + getPlatform +} \ No newline at end of file diff --git a/api/controllers/properties.controller.js b/api/controllers/properties.controller.js index a051515..022542f 100644 --- a/api/controllers/properties.controller.js +++ b/api/controllers/properties.controller.js @@ -37,17 +37,6 @@ async function allocateRam(req, res) { } } -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 serverConfig(req, res) { try { const configJSON = configUtils.getConfigJSON(); @@ -164,7 +153,6 @@ async function modifyBannedIPs(req, res) { module.exports = { toggleProperty, allocateRam, - playerCount, serverConfig, getWhitelist, getOps, diff --git a/api/controllers/server.controller.js b/api/controllers/server.controller.js index b4526d0..7af2927 100644 --- a/api/controllers/server.controller.js +++ b/api/controllers/server.controller.js @@ -1,5 +1,6 @@ const serverService = require('../services/server.service'); const configUtils = require('../utils/configUtils'); +const infoService = require('../services/info.service'); const { Sema } = require('async-sema'); @@ -39,6 +40,7 @@ async function startServer(req, res) { }else{ await serverService.startServer(); } + infoService.startCounting(); res.send('Server started.'); } catch (error) { @@ -66,6 +68,7 @@ async function stopServer(req, res) { serverStatus = 0; } + infoService.stopCounting(); res.status(200).send('Server stopped.'); } catch (error) { res.status(500).send(`Failed to stop server: ${error}`); diff --git a/api/routes/info.routes.js b/api/routes/info.routes.js new file mode 100644 index 0000000..efc5065 --- /dev/null +++ b/api/routes/info.routes.js @@ -0,0 +1,25 @@ +const express = require('express'); +const router = express.Router(); + +const { + playerCount, + getUpTime, + getMemoryUsage, + getWorldSize, + getVersion, + getPlatform +} = require('../controllers/info.controller'); + +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/properties.routes.js b/api/routes/properties.routes.js index b9f77fd..812026c 100644 --- a/api/routes/properties.routes.js +++ b/api/routes/properties.routes.js @@ -5,7 +5,6 @@ const propertiesServices = require('../services/properties.service'); const { toggleProperty, allocateRam, - playerCount, serverConfig, getWhitelist, getOps, @@ -30,8 +29,6 @@ router.put('/toggle/:property', toggleProperty); router.put('/allocate-ram/:mb', allocateRam); -router.get('/player-count', playerCount); - router.get('/server-config.json', serverConfig); router.get('/whitelist.json', getWhitelist); diff --git a/api/services/info.service.js b/api/services/info.service.js new file mode 100644 index 0000000..149237e --- /dev/null +++ b/api/services/info.service.js @@ -0,0 +1,129 @@ +const fs = require('fs'); +const path = require('path'); +const pidusage = require('pidusage') +const AdmZip = require('adm-zip'); + +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) { + const zip = new AdmZip(jarPath); + const entries = zip.getEntries(); + const names = entries.map(e => e.entryName); + + console.log(names); + + 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) { + if (getPlatform(jarPath) == "Fabric") { + console.error("Unable to get version from fabric servers"); + return "Unable to fetch version"; + } + + const zip = new AdmZip(jarPath); + const entries = zip.getEntries(); + + const versionEntry = entries.find(e => e.entryName === "version.json"); + if (versionEntry) { + try { + const content = JSON.parse(zip.readAsText(versionEntry)); + console.log(content); + 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; +} + + +module.exports = { + startCounting, + stopCounting, + getStartTime, + getMemoryUsage, + getDirectorySize, + getPlatform, + getVersion +} \ No newline at end of file diff --git a/api/services/server.service.js b/api/services/server.service.js index a372ee0..e0f49cc 100644 --- a/api/services/server.service.js +++ b/api/services/server.service.js @@ -2,14 +2,14 @@ const { spawn, exec } = require('child_process'); const fs = require('fs'); const consts = require("../consts"); const { freemem } = require('os'); -const { getConfigAttribute } = require("../utils/configUtils"); +const configUtils = require('../utils/configUtils'); let serverProcess = null; let serverStatus = consts.serverStatus.OFFLINE; async function isServerOn() { try { - const serverPID = getConfigAttribute("os") != "Linux" ? await getStrayServerInstance_WINDOWS() : await getStrayServerInstance_LINUX(); + const serverPID = configUtils.getConfigAttribute("os") != "Linux" ? await getStrayServerInstance_WINDOWS() : await getStrayServerInstance_LINUX(); return serverPID || serverStatus == consts.serverStatus.RUNNING; } catch (error) { return false; @@ -109,7 +109,7 @@ function getStrayServerInstance_WINDOWS() { if (code !== 0) reject(`netstat command failed with code ${code}`); else { - const port = getConfigAttribute("port"); + const port = configUtils.getConfigAttribute("port"); const regex = new RegExp(`TCP\\s+.*:${port}\\s+.*\\s+LISTENING\\s+(\\d+)`, 'i'); const match = netstatOutput.match(regex); @@ -163,7 +163,7 @@ function getStrayServerInstance_LINUX() { 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) { @@ -185,7 +185,7 @@ function getStrayServerInstance_LINUX() { async function killStrayServerInstance() { try { - if (getConfigAttribute("os") == "Linux") { + if (configUtils.getConfigAttribute("os") == "Linux") { const strayServerPID = await getStrayServerInstance_LINUX(); const command = `kill -9 ${strayServerPID}`; } else { @@ -216,7 +216,6 @@ function validateMemory() { try { const launchConfig = require("../server-config.json"); const availableMemory = Math.floor(freemem() / 1048576); - if (availableMemory > parseInt(launchConfig["memory"].replace("M",""))) { console.log(`${launchConfig["memory"]} Available for use!`); @@ -237,7 +236,8 @@ async function startServer() { 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 @@ -289,6 +289,10 @@ async function doesServerJarAlreadyExist() { return fs.existsSync("../server/server.jar"); } +function getServerProcess() { + return serverProcess; +} + module.exports = { isServerOn, getStrayServerInstance_WINDOWS, @@ -303,5 +307,6 @@ module.exports = { isEULAsigned, startServerWithScript, serverStatus, - deleteServerOutput + deleteServerOutput, + getServerProcess }; \ No newline at end of file From 96493a3e56c1185d0200bcc87fa8e267d6d1cc67 Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Sat, 28 Jun 2025 14:58:17 +0300 Subject: [PATCH 13/18] refactor(api): Rename all utils to convention --- api/cleaner.js | 4 ++-- api/controllers/installations.controller.js | 2 +- api/controllers/properties.controller.js | 4 ++-- api/controllers/server.controller.js | 2 +- api/routes/admin.routes.js | 2 +- api/server.js | 2 +- api/services/installations.service.js | 2 +- api/services/properties.service.js | 2 +- api/services/server.service.js | 2 +- api/starter.js | 6 +++--- api/utils/{apiAccessUtils.js => access.util.js} | 0 api/utils/{configUtils.js => config.util.js} | 0 api/utils/{jsonFilesUtils.js => files.util.js} | 0 api/utils/{installationsUtils.js => installations.util.js} | 0 api/utils/{networkingUtils.js => networking.util.js} | 2 +- .../{platformURLFetcherUtil.js => url_fetcher.util.js} | 0 16 files changed, 15 insertions(+), 15 deletions(-) rename api/utils/{apiAccessUtils.js => access.util.js} (100%) rename api/utils/{configUtils.js => config.util.js} (100%) rename api/utils/{jsonFilesUtils.js => files.util.js} (100%) rename api/utils/{installationsUtils.js => installations.util.js} (100%) rename api/utils/{networkingUtils.js => networking.util.js} (98%) rename api/utils/{platformURLFetcherUtil.js => url_fetcher.util.js} (100%) diff --git a/api/cleaner.js b/api/cleaner.js index 99b8b0c..c5dad02 100644 --- a/api/cleaner.js +++ b/api/cleaner.js @@ -1,6 +1,6 @@ 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() { diff --git a/api/controllers/installations.controller.js b/api/controllers/installations.controller.js index 53a28e9..f7182e7 100644 --- a/api/controllers/installations.controller.js +++ b/api/controllers/installations.controller.js @@ -1,5 +1,5 @@ const installationsService = require('../services/installations.service'); -const { updateConfigAttribute } = require('../utils/configUtils'); +const { updateConfigAttribute } = require('../utils/config.util'); const { Sema } = require('async-sema'); const downloadSema = new Sema(1); diff --git a/api/controllers/properties.controller.js b/api/controllers/properties.controller.js index 022542f..72d0cd9 100644 --- a/api/controllers/properties.controller.js +++ b/api/controllers/properties.controller.js @@ -1,6 +1,6 @@ const propertiesService = require("../services/properties.service"); -const configUtils = require("../utils/configUtils"); -const jsonFilesUtils = require("../utils/jsonFilesUtils"); +const configUtils = require("../utils/config.util"); +const jsonFilesUtils = require("../utils/files.util"); const { Sema } = require('async-sema'); diff --git a/api/controllers/server.controller.js b/api/controllers/server.controller.js index 7af2927..c997658 100644 --- a/api/controllers/server.controller.js +++ b/api/controllers/server.controller.js @@ -1,5 +1,5 @@ const serverService = require('../services/server.service'); -const configUtils = require('../utils/configUtils'); +const configUtils = require('../utils/config.util'); const infoService = require('../services/info.service'); const { Sema } = require('async-sema'); diff --git a/api/routes/admin.routes.js b/api/routes/admin.routes.js index 3c96641..fed5f2a 100644 --- a/api/routes/admin.routes.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/server.js b/api/server.js index b1aa4ab..56bd281 100644 --- a/api/server.js +++ b/api/server.js @@ -1,6 +1,6 @@ const http = require('http'); const { app } = require('./app'); -const configUtils = require('./utils/configUtils') +const configUtils = require('./utils/config.util') // Util files const starter = require('./starter'); diff --git a/api/services/installations.service.js b/api/services/installations.service.js index 11028a2..ea691bd 100644 --- a/api/services/installations.service.js +++ b/api/services/installations.service.js @@ -1,5 +1,5 @@ const urlFetcher = require("../utils/platformURLFetcherUtil"); -const { writeDownloadedFile } = require("../utils/installationsUtils"); +const { writeDownloadedFile } = require("../utils/installations.util"); async function downloadRouter(platform, version) { let response; diff --git a/api/services/properties.service.js b/api/services/properties.service.js index 7337f1b..cfabece 100644 --- a/api/services/properties.service.js +++ b/api/services/properties.service.js @@ -1,6 +1,6 @@ const consts = require("../consts"); const fs = require("fs"); -const { getConfigAttribute } = require("../utils/configUtils"); +const { getConfigAttribute } = require("../utils/config.util"); const { spawn } = require('child_process'); async function getProperties(){ diff --git a/api/services/server.service.js b/api/services/server.service.js index e0f49cc..4eab5c8 100644 --- a/api/services/server.service.js +++ b/api/services/server.service.js @@ -2,7 +2,7 @@ const { spawn, exec } = require('child_process'); const fs = require('fs'); const consts = require("../consts"); const { freemem } = require('os'); -const configUtils = require('../utils/configUtils'); +const configUtils = require('../utils/config.util'); let serverProcess = null; let serverStatus = consts.serverStatus.OFFLINE; 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/configUtils.js b/api/utils/config.util.js similarity index 100% rename from api/utils/configUtils.js rename to api/utils/config.util.js 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/installationsUtils.js b/api/utils/installations.util.js similarity index 100% rename from api/utils/installationsUtils.js rename to api/utils/installations.util.js 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 From 401b3d2f3fc43f71fbdb8243bf4e75c9f6f99b2b Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Sat, 28 Jun 2025 15:04:21 +0300 Subject: [PATCH 14/18] ci: Update the yml to test on their respective branches --- .github/workflows/node.js.yml | 8 ++++++++ 1 file changed, 8 insertions(+) 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: From cd619039c3ba263a96512fb46dc06300dca05a7c Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Sat, 28 Jun 2025 15:35:13 +0300 Subject: [PATCH 15/18] fix(api): Rename include to actual file name --- api/services/installations.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/services/installations.service.js b/api/services/installations.service.js index ea691bd..b8ba44c 100644 --- a/api/services/installations.service.js +++ b/api/services/installations.service.js @@ -1,4 +1,4 @@ -const urlFetcher = require("../utils/platformURLFetcherUtil"); +const urlFetcher = require("../utils/url_fetcher.util"); const { writeDownloadedFile } = require("../utils/installations.util"); async function downloadRouter(platform, version) { From 3775c938afe8d907459427bc820c0afee4977604 Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Sat, 28 Jun 2025 15:35:46 +0300 Subject: [PATCH 16/18] feat(api): Create `getAllInfo` endpoint --- api/controllers/info.controller.js | 32 ++++++------- api/routes/info.routes.js | 5 +- api/services/info.service.js | 73 ++++++++++++++++++++++++++++-- 3 files changed, 87 insertions(+), 23 deletions(-) diff --git a/api/controllers/info.controller.js b/api/controllers/info.controller.js index 636232e..c4b9eda 100644 --- a/api/controllers/info.controller.js +++ b/api/controllers/info.controller.js @@ -16,24 +16,8 @@ async function playerCount(req, res) { async function getUpTime(req, res) { try { - if (infoService.getStartTime() === null) { - res.status(200).send({ uptime: "0s" }); - return; - } - const ms = Date.now() - infoService.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`); - - res.status(200).send({ uptime: formatted.join(" ") }); + res.status(200).send(await infoService.getUpTime()); } catch (error) { console.error(error); res.status(500).send("error.. " + error.message); @@ -80,11 +64,23 @@ async function getPlatform(req, res) { } } +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 + getPlatform, + getAllInfo } \ No newline at end of file diff --git a/api/routes/info.routes.js b/api/routes/info.routes.js index efc5065..328a956 100644 --- a/api/routes/info.routes.js +++ b/api/routes/info.routes.js @@ -7,9 +7,12 @@ const { getMemoryUsage, getWorldSize, getVersion, - getPlatform + getPlatform, + getAllInfo } = require('../controllers/info.controller'); +router.get('/', getAllInfo); + router.get('/player-count', playerCount); router.get('/uptime', getUpTime); diff --git a/api/services/info.service.js b/api/services/info.service.js index 149237e..2bafe5c 100644 --- a/api/services/info.service.js +++ b/api/services/info.service.js @@ -61,8 +61,6 @@ function getPlatform(jarPath) { const entries = zip.getEntries(); const names = entries.map(e => e.entryName); - console.log(names); - const has = (file) => names.includes(file); // Detect Paper @@ -100,7 +98,6 @@ function getVersion(jarPath) { if (versionEntry) { try { const content = JSON.parse(zip.readAsText(versionEntry)); - console.log(content); return content.name || content.id || null; } catch (err) { console.warn("Failed to parse version.json:", err.message); @@ -117,6 +114,72 @@ function getVersion(jarPath) { 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; + let uptimeMs = 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, @@ -125,5 +188,7 @@ module.exports = { getMemoryUsage, getDirectorySize, getPlatform, - getVersion + getVersion, + getInfo, + getUpTime } \ No newline at end of file From 03cbbe8da9e6bdcc72e9d27a143b858a578076be Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Sat, 28 Jun 2025 18:48:34 +0300 Subject: [PATCH 17/18] feat(api): Delete old files when --- api/controllers/installations.controller.js | 4 +- api/services/info.service.js | 7 ++- api/services/installations.service.js | 52 +++++++++++++++++++-- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/api/controllers/installations.controller.js b/api/controllers/installations.controller.js index f7182e7..64ea930 100644 --- a/api/controllers/installations.controller.js +++ b/api/controllers/installations.controller.js @@ -5,8 +5,8 @@ const { Sema } = require('async-sema'); const downloadSema = new Sema(1); async function downloadServer(req, res) { - await downloadSema.acquire(); - + await downloadSema.acquire(); + try { await installationsService.downloadRouter(req.params.platform, req.params.version); res.status(201).send('Downloaded Successfully'); diff --git a/api/services/info.service.js b/api/services/info.service.js index 2bafe5c..77ade60 100644 --- a/api/services/info.service.js +++ b/api/services/info.service.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); const pidusage = require('pidusage') const AdmZip = require('adm-zip'); +const consts = require('../consts'); let startTime = null; @@ -56,7 +57,7 @@ function getDirectorySize(folderPath) { return sizeInMB; } -function getPlatform(jarPath) { +function getPlatform(jarPath = consts.serverDirectory + "/" + consts.serverName) { const zip = new AdmZip(jarPath); const entries = zip.getEntries(); const names = entries.map(e => e.entryName); @@ -115,9 +116,8 @@ function getVersion(jarPath) { } async function getUpTime() { - if (getStartTime() === null) { + if (getStartTime() === null) return { uptime: "0s" }; - } const ms = Date.now() - getStartTime(); @@ -139,7 +139,6 @@ async function getInfo(serverProcess, jarPath, folderPath) { let platform = null; let version = null; let directorySizeMB = null; - let uptimeMs = null; try { memoryUsage = await getMemoryUsage(serverProcess); diff --git a/api/services/installations.service.js b/api/services/installations.service.js index b8ba44c..ee1d8b2 100644 --- a/api/services/installations.service.js +++ b/api/services/installations.service.js @@ -1,10 +1,55 @@ 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) { + switch (platform) { case "vanilla": response = await fetch(await urlFetcher.fetchVanillaURL(version)); break; @@ -17,9 +62,10 @@ async function downloadRouter(platform, version) { case "forge": response = await fetch(await urlFetcher.fetchForgeURL(version)); break; - case _: - throw new Error(`Invalid platform --> ${platform}`) + default: + throw new Error(`Invalid platform --> ${platform}`); } + await writeDownloadedFile(response, version, platform.toUpperCase()); } catch (error) { console.error(error); From bcfeb76a049148cfd4ca3e7ef64549ed753722cc Mon Sep 17 00:00:00 2001 From: OmarSherif06 Date: Sat, 28 Jun 2025 19:02:22 +0300 Subject: [PATCH 18/18] fix(api): Correctly detect Minecraft version for Fabric servers --- api/services/info.service.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/api/services/info.service.js b/api/services/info.service.js index 77ade60..edc0e8e 100644 --- a/api/services/info.service.js +++ b/api/services/info.service.js @@ -87,14 +87,16 @@ function getPlatform(jarPath = consts.serverDirectory + "/" + consts.serverName) } function getVersion(jarPath) { - if (getPlatform(jarPath) == "Fabric") { - console.error("Unable to get version from fabric servers"); - return "Unable to fetch version"; - } - + 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 {