diff --git a/package.json b/package.json index bf9e245b..c580251b 100755 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "postinstall": "npm rebuild @tensorflow/tfjs-node --build-from-source && node ./scripts/postinstall.js", "release": "node ./scripts/release.js", + "wrapper:dev": "node ./packages/wrapper/src/index.js --dev", "dev": "concurrently -k -n Client,Server,MarketplaceServer,ChatServer,FileServer,MusicServer -c bgCyan,auto \"yarn dev:client\" \"yarn dev:server\" \"yarn dev:marketplace_server\" \"yarn dev:chat_server\" \"yarn dev:file_server\" \"yarn dev:music_server\"", "dev:file_server": "cd packages/file_server && yarn dev", "dev:music_server": "cd packages/music_server && yarn dev", @@ -31,4 +32,4 @@ "pm2": "5.3.0" }, "version": "0.49.0" -} \ No newline at end of file +} diff --git a/packages/wrapper/package.json b/packages/wrapper/package.json index bdb48a58..d500700c 100755 --- a/packages/wrapper/package.json +++ b/packages/wrapper/package.json @@ -9,12 +9,16 @@ "dependencies": { "7zip-min": "^1.4.3", "@octokit/rest": "^19.0.4", - "cors": "2.8.5", "axios": "^0.27.2", + "chalk": "4.1.2", + "cors": "2.8.5", "dotenv": "^16.0.3", - "express": "^4.18.1" + "express": "^4.18.1", + "http-proxy-middleware": "^2.0.6", + "module-alias": "^2.2.3", + "pm2": "^5.3.0" }, "devDependencies": { "corenode": "^0.28.26" } -} \ No newline at end of file +} diff --git a/packages/wrapper/Dockerfile b/packages/wrapper/src/Dockerfile similarity index 100% rename from packages/wrapper/Dockerfile rename to packages/wrapper/src/Dockerfile diff --git a/packages/wrapper/src/ascii.js b/packages/wrapper/src/ascii.js new file mode 100644 index 00000000..a9881a92 --- /dev/null +++ b/packages/wrapper/src/ascii.js @@ -0,0 +1 @@ +module.exports = " _ \r\n | | \r\n ___ ___ _ __ ___ | |_ _ _ \r\n \/ __\/ _ \\| \'_ ` _ \\| __| | | |\r\n | (_| (_) | | | | | | |_| |_| |\r\n \\___\\___\/|_| |_| |_|\\__|\\__, |\r\n __\/ |\r\n |___\/ " \ No newline at end of file diff --git a/packages/wrapper/src/globals.js b/packages/wrapper/src/globals.js new file mode 100644 index 00000000..daba5911 --- /dev/null +++ b/packages/wrapper/src/globals.js @@ -0,0 +1,22 @@ +require("dotenv").config() + +const path = require("path") +const moduleAlias = require("module-alias") + +global.packagejson = require("../package.json") +global.__root = path.resolve(__dirname) +global.isProduction = process.env.NODE_ENV === "production" +global.remoteRepo = "ragestudio/comty" +global.cachePath = path.join(process.cwd(), "cache") +global.distPath = path.join(process.cwd(), "dist") + +const aliases = { + "@shared-classes": path.resolve(__dirname, "_shared/classes"), + "@shared-lib": path.resolve(__dirname, "_shared/lib"), +} + +if (!global.isProduction) { + aliases["@shared-classes"] = path.resolve(__dirname, "shared-classes") +} + +moduleAlias.addAliases(aliases) \ No newline at end of file diff --git a/packages/wrapper/src/index.js b/packages/wrapper/src/index.js index f7f8752a..bdd3386c 100755 --- a/packages/wrapper/src/index.js +++ b/packages/wrapper/src/index.js @@ -1,91 +1,331 @@ -require("dotenv").config() +require("./globals") -const packagejson = require("../package.json") +const pm2 = require("pm2") const fs = require("fs") const path = require("path") const express = require("express") const cors = require("cors") +const chalk = require("chalk") +const { exec, spawn, fork } = require("child_process") + +const getInternalIp = require("./lib/getInternalIp") +const comtyAscii = require("./ascii") + +const useLogger = require("./lib/useLogger") +const { createProxyMiddleware } = require("http-proxy-middleware") const { setupLatestRelease } = require("./lib/setupDist") -global.remoteRepo = "ragestudio/comty" -global.cachePath = path.join(process.cwd(), "cache") -global.distPath = path.join(process.cwd(), "dist") +const developmentServers = [ + { + name: "WebAPP", + color: "bgRed", + cwd: path.resolve(global.__root, "../../app"), + }, + { + name: "MainAPI", + color: "bgBlue", + cwd: path.resolve(global.__root, "../../server"), + }, + { + name: "ChatAPI", + color: "bgMagenta", + cwd: path.resolve(global.__root, "../../chat_server"), + }, + { + name: "MarketplaceAPI", + color: "bgCyan", + cwd: path.resolve(global.__root, "../../marketplace_server"), + }, + { + name: "MusicAPI", + color: "bgGreen", + cwd: path.resolve(global.__root, "../../music_server") + }, + { + name: "FileAPI", + color: "bgYellow", + cwd: path.resolve(global.__root, "../../file_server"), + }, +] -function checkDistIntegrity() { - // check if dist folder exists - if (!fs.existsSync(global.distPath)) { - return false +const ApiServers = [ + { + name: "default", + remote: ({ + address, + protocol, + port + } = {}) => `${protocol ?? "http"}://${address ?? process.env.LOCALHOST}:${port ?? 3010}`, + }, + { + name: "chat", + remote: ({ + address, + protocol, + port + } = {}) => `${protocol ?? "http"}://${address ?? process.env.LOCALHOST}:${port ?? 3020}`, + }, + { + name: "livestreaming", + remote: ({ + address, + protocol, + port + } = {}) => `${protocol ?? "http"}://${address ?? process.env.LOCALHOST}:${port ?? 3030}`, + }, + { + name: "marketplace", + remote: ({ + address, + protocol, + port + } = {}) => `${protocol ?? "http"}://${address ?? process.env.LOCALHOST}:${port ?? 3040}` + }, + { + name: "music", + remote: ({ + address, + protocol, + port + } = {}) => `${protocol ?? "http"}://${address ?? process.env.LOCALHOST}:${port ?? 3050}` + }, + { + name: "files", + remote: ({ + address, + protocol, + port + } = {}) => `${protocol ?? "http"}://${address ?? process.env.LOCALHOST}:${port ?? 3060}` } +] - // TODO: check the dist checksum with oficial server checksum - - return true -} - -function fetchDistManifest() { - if (!fs.existsSync(global.distPath)) { - return null - } - - const pkgPath = path.join(global.distPath, "manifest.json") - - if (!fs.existsSync(pkgPath)) { - return null - } - - const pkg = require(pkgPath) - - return pkg -} - -async function runServer() { - const app = express() - - const portFromArgs = process.argv[2] - let portListen = portFromArgs ? portFromArgs : 9000 - - app.use(cors({ - origin: "*", - methods: "GET,HEAD,PUT,PATCH,POST,DELETE", - preflightContinue: true, - optionsSuccessStatus: 204 - })) - - app.use(express.static(global.distPath)) - - app.get("/_dist_manifest", async (req, res) => { - const manifest = fetchDistManifest() - - if (!manifest) { - return res.status(500).send("Dist not found") +class Main { + static checkDistIntegrity() { + // check if dist folder exists + if (!fs.existsSync(global.distPath)) { + return false } - return res.json(manifest) - }) + // TODO: check the dist checksum with oficial server checksum - app.get("*", function (req, res) { - res.sendFile(path.join(global.distPath, "index.html")) - }) - - app.listen(portListen) - - console.log(`Running Wrapper v${packagejson.version}`) - console.log(`🌐 Listening app in port [${portListen}]`) -} - -async function main() { - // check if dist is valid - if (!checkDistIntegrity()) { - console.log("DistIntegrity is not valid, downloading latest release...") - await setupLatestRelease() + return true } - // start app - await runServer() + static fetchDistManifest() { + if (!fs.existsSync(global.distPath)) { + return null + } + + const pkgPath = path.join(global.distPath, "manifest.json") + + if (!fs.existsSync(pkgPath)) { + return null + } + + const pkg = require(pkgPath) + + return pkg + } + + constructor(process) { + this.process = process + this.args = this.getArgs() + + this.registerExitHandlers() + this.initialize() + } + + initialize = async () => { + console.clear() + console.log(comtyAscii) + console.log(`${chalk.bgBlue(`Running Wrapper`)} ${chalk.bgMagenta(`[v${global.packagejson.version}]`)}`) + + this.internalIp = await getInternalIp() + + this.webapp_port = this.args.web_port ?? 9000 + this.api_port = this.args.api_proxy_port ?? 5000 + + if (this.args.dev === true) { + console.log(`🔧 Running in ${chalk.bgYellow("DEVELOPMENT")} mode \n\n`) + + //this.runDevelopmentServers() + this.runDevelopmentScript() + this.initializeAPIProxyServer() + + return this + } else { + if (!Main.checkDistIntegrity()) { + await setupLatestRelease() + } + } + + this.initializeWebAppServer() + this.initializeAPIProxyServer() + + return this + } + + runDevelopmentScript = async () => { + const devScript = spawn("npm", ["run", "dev"], { + cwd: path.resolve(global.__root, "../../../"), + shell: true, + stdio: "inherit" + }) + + // devScript.stdout.on("data", (data) => { + // console.log(`${chalk.bgYellow("[WebAPP]")} ${data.toString()}`) + // }) + + devScript.on("exit", (code) => { + console.log(`🔧 ${chalk.bgYellow("WebAPP")} exited with code ${code}`) + }) + } + + runDevelopmentServers = async () => { + this.dev_servers = [] + + // start all development servers + for (let i = 0; i < developmentServers.length; i++) { + const server = developmentServers[i] + + console.log(`🔧 Starting ${chalk.bgYellow(server.name)}...`) + + const serverProcess = spawn("npm", ["run", "dev"], { + cwd: server.cwd, + shell: true + }) + + let chalkInstance = chalk[server.color] + + if (typeof chalkInstance === undefined) { + chalkInstance = chalk.bgWhite + } + + // log output of server + serverProcess.stdout.on("data", (data) => { + console.log(`${chalkInstance(`[${server.name}]`)} ${data.toString()}`) + }) + + serverProcess.on("exit", (code) => { + console.log(`🔧 ${chalk.bgYellow(server.name)} exited with code ${code}`) + }) + + this.dev_servers.push({ + name: server.name, + process: serverProcess + }) + } + } + + registerExitHandlers() { + this.process.on("exit", this.onExit) + this.process.on("SIGINT", this.onExit) + this.process.on("SIGUSR1", this.onExit) + this.process.on("SIGUSR2", this.onExit) + this.process.on("uncaughtException", this.onExit) + } + + onExit = async () => { + console.clear() + console.log(comtyAscii) + console.log(`Closing wrapper... \n\n`) + + setTimeout(() => { + console.log(`Wrapper did not close in time, forcefully closing...`) + process.exit(0) + }, 5000) + + if (Array.isArray(this.dev_servers)) { + for await (const server of this.dev_servers) { + console.log(`Killing ${chalk.bgYellow(server.name)}...`) + + server.process.kill() + } + } + + return process.exit(0) + } + + getArgs = () => { + let args = {} + + for (let i = 0; i < this.process.argv.length; i++) { + const arg = this.process.argv[i] + + if (arg.startsWith("--")) { + const argName = arg.replace("--", "") + const argValue = this.process.argv[i + 1] + + args[argName] = argValue ?? true + } + } + + return args + } + + initializeWebAppServer = async () => { + this.webapp_server = express() + + this.webapp_server.use(cors({ + origin: "*", + methods: "GET,HEAD,PUT,PATCH,POST,DELETE", + preflightContinue: true, + optionsSuccessStatus: 204 + })) + + if (!this.forwardAppPort) { + this.webapp_server.use(express.static(global.distPath)) + + this.webapp_server.get("*", (req, res) => { + return res.sendFile(path.join(global.distPath, "index.html")) + }) + } else { + this.webapp_server.use(createProxyMiddleware({ + target: `http://${this.internalIp}:${this.forwardAppPort}`, + changeOrigin: true, + ws: true, + pathRewrite: { + "^/": "" + } + })) + } + + this.webapp_server.listen(this.webapp_port) + + console.log(`🌐 WEB-APP Listening on > `, `${this.internalIp}:${this.webapp_port}`) + + return this.webapp_server + } + + initializeAPIProxyServer = async () => { + this.apiproxy_server = express() + + this.apiproxy_server.use(useLogger) + + ApiServers.forEach((server) => { + const remote = server.remote({ + address: "eu02.ragestudio.net", //this.internalIp, + protocol: this.forceApiHttps ? "https" : "http", + }) + + this.apiproxy_server.use(`/${server.name}`, createProxyMiddleware({ + target: `${remote}`, + changeOrigin: true, + ws: true, + pathRewrite: { + [`^/${server.name}`]: "" + } + })) + }) + + this.apiproxy_server.listen(this.api_port) + + console.log(`🌐 API-PROXY Listening on >`, `${this.internalIp}:${this.api_port}`) + + return this.apiproxy_server + } } -main().catch((err) => { - console.error(`[FATAL ERROR] >`, err) -}) \ No newline at end of file +new Main(process) \ No newline at end of file diff --git a/packages/wrapper/src/lib/getInternalIp.js b/packages/wrapper/src/lib/getInternalIp.js new file mode 100644 index 00000000..f1024394 --- /dev/null +++ b/packages/wrapper/src/lib/getInternalIp.js @@ -0,0 +1,12 @@ +const dns = require("dns") +const os = require("os") + +module.exports = () => new Promise((resolve, reject) => { + dns.lookup(os.hostname(), (err, address, family) => { + if (err) { + reject(err) + } + + resolve(address) + }) +}) \ No newline at end of file diff --git a/packages/wrapper/src/lib/useLogger/index.js b/packages/wrapper/src/lib/useLogger/index.js new file mode 100644 index 00000000..fa33f9b2 --- /dev/null +++ b/packages/wrapper/src/lib/useLogger/index.js @@ -0,0 +1,20 @@ +// just works with express +module.exports = (req, res, next) => { + const startHrTime = process.hrtime() + + res.on("finish", () => { + const elapsedHrTime = process.hrtime(startHrTime) + const elapsedTimeInMs = elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1e6 + + res._responseTimeMs = elapsedTimeInMs + + // cut req.url if is too long + if (req.url.length > 100) { + req.url = req.url.substring(0, 100) + "..." + } + + console.log(`${req.method} ${res._status_code ?? res.statusCode ?? 200} ${req.url} ${elapsedTimeInMs}ms`) + }) + + next() +} \ No newline at end of file