From f6c7ebd468edcb23df82ee4210918d70939f70f3 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Tue, 1 Apr 2025 21:52:04 +0000 Subject: [PATCH] improve gateway --- packages/server/gateway/index.js | 43 ++-- .../gateway/managers/http-proxy/index.js | 3 +- .../server/gateway/managers/nginx/index.js | 27 ++- packages/server/gateway/repl.js | 206 +++++++++++++----- packages/server/gateway/services/manager.js | 7 +- packages/server/gateway/services/service.js | 4 + packages/server/gateway/utils/spawnService.js | 7 +- 7 files changed, 195 insertions(+), 102 deletions(-) diff --git a/packages/server/gateway/index.js b/packages/server/gateway/index.js index ef5f4a0f..3fe65318 100755 --- a/packages/server/gateway/index.js +++ b/packages/server/gateway/index.js @@ -17,7 +17,9 @@ import ServiceManager from "./services/manager" import Service from "./services/service" import * as Managers from "./managers" +global.debugFlag = process.env.DEBUG === "true" const isProduction = process.env.NODE_ENV === "production" + const sslKey = path.resolve(process.cwd(), ".ssl", "privkey.pem") const sslCert = path.resolve(process.cwd(), ".ssl", "cert.pem") @@ -71,6 +73,10 @@ export default class Gateway { * Creates and initializes all service instances */ async createServiceInstances() { + if (!debugFlag) { + console.log(`🔰 Starting all services, please wait...`) + } + for await (const servicePath of this.services) { const instanceBasePath = path.dirname(servicePath) const servicePkg = require( @@ -99,8 +105,6 @@ export default class Gateway { // Initialize service await service.initialize() - - console.log(`📦 [${serviceId}] Service initialized`) } } @@ -113,10 +117,6 @@ export default class Gateway { this.serviceRegistry[serviceId].initialized = true this.serviceRegistry[serviceId].ready = true - console.log( - `✅ [${serviceId}][${this.serviceRegistry[serviceId].index}] Ready`, - ) - // Check if all services are ready this.checkAllServicesReady() } @@ -130,7 +130,7 @@ export default class Gateway { const id = service.id if (data.type === "log") { - console.log(`[${id}] ${data.message}`) + console.log(`[ipc:${id}] ${data.message}`) } if (data.status === "ready") { @@ -234,9 +234,9 @@ export default class Gateway { //console.clear() //console.log(comtyAscii) - console.log("\n\n\n") + console.log("\n") console.log(`🎉 All services[${this.services.length}] ready!\n`) - console.log(`USE: select , reload, exit`) + console.log(`USE: select , reload, exit\n`) if (typeof this.gateway.applyConfiguration === "function") { await this.gateway.applyConfiguration() @@ -285,11 +285,21 @@ export default class Gateway { this.services = await scanServices() this.ipcRouter = new IPCRouter() + global.eventBus = this.eventBus + global.ipcRouter = this.ipcRouter + if (this.services.length === 0) { console.error("❌ No services found") return process.exit(1) } + console.log(comtyAscii) + console.log( + `\nRunning ${chalk.bgBlue(`${pkg.name}`)} | ${chalk.bgMagenta(`[v${pkg.version}]`)} | ${this.state.internalIp} | ${isProduction ? "production" : "development"} | ${this.constructor.gatewayMode} |\n`, + ) + + console.log(`📦 Found ${this.services.length} service(s)`) + // Initialize gateway this.gateway = new Managers[this.constructor.gatewayMode]({ port: this.state.proxyPort, @@ -302,19 +312,6 @@ export default class Gateway { await this.gateway.initialize() } - // Make key components available globally - global.eventBus = this.eventBus - global.ipcRouter = this.ipcRouter - global.proxy = this.proxy - - //console.clear() - console.log(comtyAscii) - console.log( - `\nRunning ${chalk.bgBlue(`${pkg.name}`)} | ${chalk.bgMagenta(`[v${pkg.version}]`)} | ${this.state.internalIp} | ${isProduction ? "production" : "development"} \n\n\n`, - ) - - console.log(`📦 Found ${this.services.length} service(s)`) - // Watch for service state changes Observable.observe(this.serviceRegistry, (changes) => { this.checkAllServicesReady() @@ -325,7 +322,7 @@ export default class Gateway { // WARNING: Starting relp makes uwebsockets unable to work properly, surging some bugs from nodejs (domain.enter) // use another alternative to parse commands, like stdin reading or something... - //this.startRELP() + this.startRELP() } startRELP() { diff --git a/packages/server/gateway/managers/http-proxy/index.js b/packages/server/gateway/managers/http-proxy/index.js index dea6f393..552e0229 100755 --- a/packages/server/gateway/managers/http-proxy/index.js +++ b/packages/server/gateway/managers/http-proxy/index.js @@ -123,7 +123,7 @@ export default class Proxy { return reject(err) } console.log( - `🚀 Gateway listening on ${this.config.port}:${this.config.internalIp}`, + `🚀 Gateway listening on ${this.config.internalIp}:${this.config.port}`, ) resolve() }, @@ -205,6 +205,7 @@ export default class Proxy { name: pkg.name, version: pkg.version, lb_version: defaults?.version || "unknown", + gateway: "standard", }), ) return diff --git a/packages/server/gateway/managers/nginx/index.js b/packages/server/gateway/managers/nginx/index.js index 35491a53..11deb046 100755 --- a/packages/server/gateway/managers/nginx/index.js +++ b/packages/server/gateway/managers/nginx/index.js @@ -2,7 +2,8 @@ import fs from "node:fs/promises" import { existsSync, mkdirSync, writeFileSync } from "node:fs" import path from "node:path" import { execSync, spawn } from "node:child_process" -import { platform } from "node:os" +import defaults from "linebridge/dist/defaults" +import pkg from "../../../package.json" const localNginxBinary = path.resolve(process.cwd(), "nginx-bin") @@ -43,9 +44,6 @@ export default class NginxManager { this.nginxProcess = null this.isNginxRunning = false - // Debug mode - this.debug = options.debug || false - if ( existsSync(this.options.cert_file_name) && existsSync(this.options.key_file_name) @@ -128,12 +126,19 @@ export default class NginxManager { .join(this.tempDir, "cache") .replace(/\\/g, "/") + const mainEndpointJSON = JSON.stringify({ + name: pkg.name, + version: "1.21.6", + lb_version: defaults?.version ?? "unknown", + gateway: "nginx", + }) + const config = ` # Nginx configuration for Comty API Gateway # Auto-generated - Do not edit manually worker_processes auto; -error_log ${normalizedLogsDir}/error.log ${this.debug ? "debug" : "error"}; +error_log ${normalizedLogsDir}/error.log ${debugFlag ? "debug" : "error"}; pid ${normalizedTempDir}/nginx.pid; events { @@ -155,7 +160,7 @@ http { 'request_time: $request_time ' 'http_version: $server_protocol'; - access_log ${normalizedLogsDir}/access.log ${this.debug ? "debug" : "main"}; + access_log ${normalizedLogsDir}/access.log ${debugFlag ? "debug" : "main"}; sendfile on; tcp_nodelay on; @@ -190,7 +195,7 @@ http { add_header 'Access-Control-Allow-Headers' '*' always; add_header 'Access-Control-Allow-Methods' 'GET,HEAD,PUT,PATCH,POST,DELETE' always; - return 200 '{"ok":1}'; + return 200 '${mainEndpointJSON}'; } # Include service-specific configurations @@ -247,7 +252,7 @@ http { ? routePath : `/${routePath}` - if (this.debug) { + if (debugFlag) { console.log( `🔍 Registering route for [${serviceId}]: ${normalizedPath} -> ${target} (${websocket ? "WebSocket" : "HTTP"})`, ) @@ -349,7 +354,7 @@ http { // Write the config await fs.writeFile(this.servicesConfigPath, config) - if (this.debug) { + if (debugFlag) { console.log(`📄 Writted [${this.routes.size}] service routes`) } } @@ -494,7 +499,7 @@ ${locationDirective} { const cmdString = `"${this.nginxBinary}" ${allArgs.join(" ")}` - if (this.debug) { + if (debugFlag) { console.log(`🔍 Executing: ${cmdString}`) } @@ -566,7 +571,7 @@ ${locationDirective} { * Stop the Nginx server * @returns {Boolean} - Success status */ - async close() { + async stop() { try { if (this.nginxProcess) { // Try graceful shutdown first diff --git a/packages/server/gateway/repl.js b/packages/server/gateway/repl.js index 32e82311..e87b4e55 100755 --- a/packages/server/gateway/repl.js +++ b/packages/server/gateway/repl.js @@ -1,68 +1,162 @@ -import repl from "node:repl" - export default class RELP { - constructor(handlers) { - this.handlers = handlers + constructor(handlers) { + this.handlers = handlers + this.initCommandLine() + } - repl.start({ - prompt: "> ", - useGlobal: true, - eval: (input, context, filename, callback) => { - let inputs = input.split(" ") + initCommandLine() { + // Configure line-by-line input mode + process.stdin.setEncoding("utf8") + process.stdin.resume() + process.stdin.setRawMode(true) - // remove last \n from input - inputs[inputs.length - 1] = inputs[inputs.length - 1].replace(/\n/g, "") + // Buffer to store user input + this.inputBuffer = "" - // find relp command - const command = inputs[0] - const args = inputs.slice(1) + // Show initial prompt + this.showPrompt() - const command_fn = this.commands.find((relp_command) => { - let exising = false + // Handle user input + process.stdin.on("data", (data) => { + const key = data.toString() - if (Array.isArray(relp_command.aliases)) { - exising = relp_command.aliases.includes(command) - } + // Ctrl+C to exit + if (key === "\u0003") { + process.exit(0) + } - if (relp_command.cmd === command) { - exising = true - } + // Enter key + if (key === "\r" || key === "\n") { + // Move to a new line + console.log() - return exising - }) + // Process the command + const command = this.inputBuffer.trim() + if (command) { + this.processCommand(command) + } - if (!command_fn) { - return callback(`Command not found: ${command}`) - } + // Clear the buffer + this.inputBuffer = "" - return command_fn.fn(callback, ...args) - } - }) - } + // Show the prompt again + this.showPrompt() + return + } - commands = [ - { - cmd: "select", - aliases: ["s", "sel"], - fn: (cb, service) => { - this.handlers.detachAllServicesSTD() + // Backspace/Delete + if (key === "\b" || key === "\x7f") { + if (this.inputBuffer.length > 0) { + // Delete a character from the buffer + this.inputBuffer = this.inputBuffer.slice(0, -1) - return this.handlers.attachServiceSTD(service) - } - }, - { - cmd: "reload", - aliases: ["r"], - fn: () => { - this.handlers.reloadService() - } - }, - { - cmd: "exit", - aliases: ["e"], - fn: () => { - process.exit(0) - } - } - ] -} \ No newline at end of file + // Update the line in the terminal + process.stdout.write("\r\x1b[K> " + this.inputBuffer) + } + return + } + + // Normal characters + if (key.length === 1 && key >= " ") { + this.inputBuffer += key + process.stdout.write(key) + } + }) + + // Intercept console.log to keep the prompt visible + const originalConsoleLog = console.log + console.log = (...args) => { + // Clear the current line + process.stdout.write("\r\x1b[K") + + // Print the message + originalConsoleLog(...args) + + // Reprint the prompt and current buffer + this.showPrompt(false) + } + + // Do the same with console.error + const originalConsoleError = console.error + console.error = (...args) => { + // Clear the current line + process.stdout.write("\r\x1b[K") + + // Print the error message + originalConsoleError(...args) + + // Reprint the prompt and current buffer + this.showPrompt(false) + } + } + + showPrompt(newLine = true) { + if (newLine) { + process.stdout.write("\r") + } + process.stdout.write("> " + this.inputBuffer) + } + + processCommand(input) { + const inputs = input.split(" ") + const command = inputs[0] + const args = inputs.slice(1) + + this.inputBuffer = "" + + const commandFn = this.commands.find((relpCommand) => { + if (relpCommand.cmd === command) { + return true + } + + if (Array.isArray(relpCommand.aliases)) { + return relpCommand.aliases.includes(command) + } + + return false + }) + + if (!commandFn) { + console.error(`Command not found: ${command}`) + return + } + + // Adapter to maintain compatibility with the original API + const callback = (result) => { + if (result) { + console.log(result) + } + } + + try { + commandFn.fn(callback, ...args) + } catch (error) { + console.error(`Error executing command: ${error.message}`) + } + } + + commands = [ + { + cmd: "select", + aliases: ["s", "sel"], + fn: (cb, service) => { + this.handlers.detachAllServicesSTD() + return this.handlers.attachServiceSTD(service) + }, + }, + { + cmd: "reload", + aliases: ["r"], + fn: () => { + this.handlers.reloadService() + }, + }, + { + cmd: "exit", + aliases: ["e"], + fn: () => { + process.exit(0) + }, + }, + ] +} diff --git a/packages/server/gateway/services/manager.js b/packages/server/gateway/services/manager.js index d429482d..a0b43f8c 100755 --- a/packages/server/gateway/services/manager.js +++ b/packages/server/gateway/services/manager.js @@ -51,6 +51,7 @@ export default class ServiceManager { } const service = this.getService(id) + if (!service) { console.error(`Service [${id}] not found`) return false @@ -64,8 +65,6 @@ export default class ServiceManager { * Reload all services */ reloadAllServices() { - console.log("Reloading all services...") - for (const service of this.services) { service.reload() } @@ -75,8 +74,6 @@ export default class ServiceManager { * Stop all services */ stopAllServices() { - console.log("Stopping all services...") - for (const service of this.services) { service.stop() } @@ -88,8 +85,6 @@ export default class ServiceManager { * @returns {boolean} True if attachment was successful */ attachServiceStd(id) { - console.log(`Attaching to service [${id}]`) - if (id === "all") { this.selectedService = "all" this.attachAllServicesStd() diff --git a/packages/server/gateway/services/service.js b/packages/server/gateway/services/service.js index 0269f8be..a4666db2 100755 --- a/packages/server/gateway/services/service.js +++ b/packages/server/gateway/services/service.js @@ -51,6 +51,8 @@ export default class Service { * Start the service process */ async startProcess() { + console.log(`🔰 [${this.id}] Starting service...`) + this.instance = await spawnService({ id: this.id, service: this.path, @@ -60,7 +62,9 @@ export default class Service { onIPCData: this.handleIPCData.bind(this), }) + // if debug flag is enabled, attach logs on start this.instance.logs.attach() + return this.instance } diff --git a/packages/server/gateway/utils/spawnService.js b/packages/server/gateway/utils/spawnService.js index 80739474..ea1268b2 100755 --- a/packages/server/gateway/utils/spawnService.js +++ b/packages/server/gateway/utils/spawnService.js @@ -23,19 +23,16 @@ export default async ({ id, service, cwd, onClose, onError, onIPCData }) => { instance.logs = { stdout: createServiceLogTransformer({ id }), stderr: createServiceLogTransformer({ id, color: "bgRed" }), - attach: () => { + attach: (withBuffer = false) => { instance.logs.stdout.pipe(process.stdout) instance.logs.stderr.pipe(process.stderr) }, - detach: () => { + detach: (withBuffer = false) => { instance.logs.stdout.unpipe(process.stdout) instance.logs.stderr.unpipe(process.stderr) }, } - instance.logs.stdout.history = [] - instance.logs.stderr.history = [] - // push to buffer history instance.stdout.pipe(instance.logs.stdout) instance.stderr.pipe(instance.logs.stderr)