diff --git a/.github/workflows/build-supervisord.yml b/.github/workflows/build-supervisord.yml new file mode 100644 index 0000000..a2c2ca7 --- /dev/null +++ b/.github/workflows/build-supervisord.yml @@ -0,0 +1,92 @@ +name: Build & Push Pre-Dev Image + +on: + push: + branches: + - supervisord + +jobs: + send-tg-msg: + name: Send TG message + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Send Telegram message + uses: proDreams/actions-telegram-notifier@main + with: + token: ${{ secrets.TELEGRAM_TOKEN }} + chat_id: ${{ secrets.TELEGRAM_CHAT_ID }} + thread_id: ${{ secrets.TELEGRAM_TOPIC_ID }} + status: pending + notify_fields: 'repo_with_tag,commit,workflow' + title: 'Building docker image.' + + build-docker-image: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '22.x' + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v3 + with: + context: . + platforms: linux/amd64 + push: true + tags: | + remnawave/node:supervisord + + send-finish-tg-msg: + name: Send TG message + needs: [build-docker-image] + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Send Telegram message + uses: proDreams/actions-telegram-notifier@main + with: + token: ${{ secrets.TELEGRAM_TOKEN }} + chat_id: ${{ secrets.TELEGRAM_CHAT_ID }} + thread_id: ${{ secrets.TELEGRAM_TOPIC_ID }} + status: ${{ job.status }} + notify_fields: 'repo_with_tag,commit,workflow' + title: 'Build Pre-Dev finished.' + + notify-on-error: + runs-on: ubuntu-latest + needs: [build-docker-image] + if: failure() + steps: + - name: Checkout source code + uses: actions/checkout@v2 + + - name: Send error notification + uses: proDreams/actions-telegram-notifier@main + with: + token: ${{ secrets.TELEGRAM_TOKEN }} + chat_id: ${{ secrets.TELEGRAM_CHAT_ID }} + thread_id: ${{ secrets.TELEGRAM_TOPIC_ID }} + status: failure + notify_fields: 'repo_with_tag,commit,workflow' + title: 'Build Pre-Dev failed.' diff --git a/libs/contract/commands/handler/remove-user.command.ts b/libs/contract/commands/handler/remove-user.command.ts index 3dde832..f6dfbdb 100644 --- a/libs/contract/commands/handler/remove-user.command.ts +++ b/libs/contract/commands/handler/remove-user.command.ts @@ -6,7 +6,6 @@ export namespace RemoveUserCommand { export const url = REST_API.HANDLER.REMOVE_USER; export const RequestSchema = z.object({ - tags: z.array(z.string()), username: z.string(), }); diff --git a/libs/contract/constants/xray/stats.ts b/libs/contract/constants/xray/stats.ts index 1da0e46..ee81217 100644 --- a/libs/contract/constants/xray/stats.ts +++ b/libs/contract/constants/xray/stats.ts @@ -22,7 +22,8 @@ export const XRAY_DEFAULT_STATS_MODEL = { export const XRAY_DEFAULT_API_MODEL = { api: { services: ['HandlerService', 'StatsService', 'RoutingService'], - tag: 'api', + listen: '127.0.0.1:61000', + tag: 'REMNAWAVE_API', }, } as const; diff --git a/libs/contract/package.json b/libs/contract/package.json index 2edcaa9..cfc0f1e 100644 --- a/libs/contract/package.json +++ b/libs/contract/package.json @@ -1,6 +1,6 @@ { "name": "@remnawave/node-contract", - "version": "0.4.1", + "version": "0.5.0", "description": "A node-contract library for Remnawave Panel", "main": "build/index.js", "types": "build/index.d.ts", diff --git a/package-lock.json b/package-lock.json index 22c6ba8..9e68885 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@nestjs/jwt": "11.0.0", "@nestjs/passport": "11.0.5", "@nestjs/platform-express": "11.0.12", + "@remnawave/supervisord-nestjs": "0.1.1", "@remnawave/xtls-sdk": "0.3.0", "@remnawave/xtls-sdk-nestjs": "0.2.2", "enhanced-ms": "^4.1.0", @@ -26,7 +27,9 @@ "nest-winston": "^1.10.2", "nestjs-zod": "4.3.1", "node-object-hash": "^3.1.1", + "node-supervisord": "^1.0.6-rc.2", "object-hash": "^3.0.0", + "p-retry": "^6.2.1", "passport": "0.7.0", "passport-jwt": "4.0.1", "pkg-types": "^2.1.0", @@ -48,7 +51,7 @@ "@types/jsonwebtoken": "^9.0.9", "@types/mjml": "^4.7.4", "@types/morgan": "^1.9.9", - "@types/node": "^22.13.13", + "@types/node": "^22.13.14", "@types/nunjucks": "^3.2.6", "@types/object-hash": "^3.0.6", "@types/passport-jwt": "^4.0.1", @@ -1620,6 +1623,19 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, + "node_modules/@remnawave/supervisord-nestjs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@remnawave/supervisord-nestjs/-/supervisord-nestjs-0.1.1.tgz", + "integrity": "sha512-y+ek9xXIeQ4/kH/yg7/iDb6nvPNTqNI8l1E1uYUhBK/1dDhWo9Q7vpDuPwALKOhdUhotnm0yGPWVh0EoMTnqJw==", + "license": "AGPL-3.0-only", + "dependencies": { + "@nestjs/common": "^11.0.12", + "@nestjs/core": "^11.0.12" + }, + "peerDependencies": { + "node-supervisord": ">=1.0.6-rc.2" + } + }, "node_modules/@remnawave/xtls-sdk": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@remnawave/xtls-sdk/-/xtls-sdk-0.3.0.tgz", @@ -1850,9 +1866,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz", - "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==", + "version": "22.13.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", + "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", "license": "MIT", "dependencies": { "undici-types": "~6.20.0" @@ -1927,6 +1943,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "license": "MIT" + }, "node_modules/@types/semver": { "version": "7.7.0", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz", @@ -4729,6 +4751,18 @@ "node": ">=8" } }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -5604,6 +5638,19 @@ "dev": true, "license": "MIT" }, + "node_modules/node-supervisord": { + "version": "1.0.6-rc.2", + "resolved": "https://registry.npmjs.org/node-supervisord/-/node-supervisord-1.0.6-rc.2.tgz", + "integrity": "sha512-lkDcHxuuo9m4Z4o0xQgbr819t6JH0NGzSJAoiw4RN4B1Byt3BN2JlNZa3Nmy63iIVysxHcVYFI5VqQZX5m3wDA==", + "license": "MIT", + "dependencies": { + "url": "^0.11.3", + "xmlrpc": "^1.3.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -5824,6 +5871,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -6323,6 +6387,15 @@ "dev": true, "license": "ISC" }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6420,6 +6493,12 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "license": "ISC" + }, "node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", @@ -7396,6 +7475,25 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7743,6 +7841,29 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xmlbuilder": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", + "integrity": "sha512-eKRAFz04jghooy8muekqzo8uCSVNeyRedbuJrp0fovbLIi7wlsYtdUn3vBAAPq2Y3/0xMz2WMEUQ8yhVVO9Stw==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlrpc": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz", + "integrity": "sha512-jQf5gbrP6wvzN71fgkcPPkF4bF/Wyovd7Xdff8d6/ihxYmgETQYSuTc+Hl+tsh/jmgPLro/Aro48LMFlIyEKKQ==", + "license": "MIT", + "dependencies": { + "sax": "1.2.x", + "xmlbuilder": "8.2.x" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.0.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 18f4482..af10367 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@nestjs/jwt": "11.0.0", "@nestjs/passport": "11.0.5", "@nestjs/platform-express": "11.0.12", + "@remnawave/supervisord-nestjs": "0.1.1", "@remnawave/xtls-sdk": "0.3.0", "@remnawave/xtls-sdk-nestjs": "0.2.2", "enhanced-ms": "^4.1.0", @@ -44,7 +45,9 @@ "nest-winston": "^1.10.2", "nestjs-zod": "4.3.1", "node-object-hash": "^3.1.1", + "node-supervisord": "^1.0.6-rc.2", "object-hash": "^3.0.0", + "p-retry": "^6.2.1", "passport": "0.7.0", "passport-jwt": "4.0.1", "pkg-types": "^2.1.0", @@ -66,7 +69,7 @@ "@types/jsonwebtoken": "^9.0.9", "@types/mjml": "^4.7.4", "@types/morgan": "^1.9.9", - "@types/node": "^22.13.13", + "@types/node": "^22.13.14", "@types/nunjucks": "^3.2.6", "@types/object-hash": "^3.0.6", "@types/passport-jwt": "^4.0.1", @@ -85,4 +88,4 @@ "tsconfig-paths": "^4.2.0", "typescript": "~5.8.2" } -} \ No newline at end of file +} diff --git a/src/app.module.ts b/src/app.module.ts index 0ae6d88..eaf1fe2 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,6 +2,7 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { Module } from '@nestjs/common'; import { JwtModule } from '@nestjs/jwt'; +import { SupervisordNestjsModule } from '@remnawave/supervisord-nestjs'; import { XtlsSdkNestjsModule } from '@remnawave/xtls-sdk-nestjs'; import { JwtStrategy } from '@common/guards/jwt-guards/strategies/validate-token'; @@ -28,6 +29,17 @@ import { InternalModule } from './modules/internal/internal.module'; port: configService.getOrThrow('XTLS_PORT'), }), }), + SupervisordNestjsModule.forRootAsync({ + imports: [], + inject: [], + useFactory: () => ({ + host: 'http://localhost:61002', + options: { + username: 'remnawave', + password: 'glcmYQLRwPXDXIBq', + }, + }), + }), RemnawaveNodeModules, InternalModule, JwtModule.registerAsync(getJWTConfig()), diff --git a/src/common/exception/httpException.filter.ts b/src/common/exception/httpException.filter.ts index 7927c4f..5057469 100644 --- a/src/common/exception/httpException.filter.ts +++ b/src/common/exception/httpException.filter.ts @@ -1,7 +1,8 @@ -import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, Logger } from '@nestjs/common'; import { ZodValidationException } from 'nestjs-zod'; import { Request, Response } from 'express'; +import { ArgumentsHost, Catch, ExceptionFilter, HttpStatus, Logger } from '@nestjs/common'; + import { HttpExceptionWithErrorCodeType } from './http-exeception-with-error-code.type'; @Catch(HttpExceptionWithErrorCodeType, ZodValidationException) diff --git a/src/common/utils/filter-logs/filter-logs.ts b/src/common/utils/filter-logs/filter-logs.ts new file mode 100644 index 0000000..40041b3 --- /dev/null +++ b/src/common/utils/filter-logs/filter-logs.ts @@ -0,0 +1,13 @@ +import winston from 'winston'; + +const contextsToIgnore = ['InstanceLoader', 'RoutesResolver', 'RouterExplorer']; + +export const customLogFilter = winston.format((info) => { + if (info.context) { + const contextValue = String(info.context); + if (contextsToIgnore.some((ctx) => contextValue === ctx)) { + return false; + } + } + return info; +}); diff --git a/src/common/utils/filter-logs/index.ts b/src/common/utils/filter-logs/index.ts new file mode 100644 index 0000000..894aad2 --- /dev/null +++ b/src/common/utils/filter-logs/index.ts @@ -0,0 +1 @@ +export * from './filter-logs'; diff --git a/src/common/utils/generate-api-config.ts b/src/common/utils/generate-api-config.ts index b47bab1..1cdcc3c 100644 --- a/src/common/utils/generate-api-config.ts +++ b/src/common/utils/generate-api-config.ts @@ -1,5 +1,4 @@ import { - XRAY_API_INBOUND_MODEL, XRAY_DEFAULT_API_MODEL, XRAY_DEFAULT_POLICY_MODEL, XRAY_DEFAULT_STATS_MODEL, @@ -18,9 +17,9 @@ export const generateApiConfig = (config: Record): Record> { try { const { data: requestData } = data; const response: Array> = []; + const inboundsTags = this.xrayService.getSavedInboundsTags(); + + for (const tag of inboundsTags) { + this.logger.debug(`Removing user: ${requestData[0].username} from tag: ${tag}`); + await this.xtlsApi.handler.removeUser(tag, requestData[0].username); + } + for (const item of requestData) { let tempRes = null; @@ -126,9 +138,12 @@ export class HandlerService { data: IRemoveUserRequest, ): Promise> { try { - const { username, tags } = data; + const { username } = data; const response: Array> = []; - for (const tag of tags) { + + const inboundsTags = this.xrayService.getSavedInboundsTags(); + + for (const tag of inboundsTags) { const tempRes = await this.xtlsApi.handler.removeUser(tag, username); response.push(tempRes); } diff --git a/src/modules/handler/interfaces/remove-user.interface.ts b/src/modules/handler/interfaces/remove-user.interface.ts index 85fffba..b9129fd 100644 --- a/src/modules/handler/interfaces/remove-user.interface.ts +++ b/src/modules/handler/interfaces/remove-user.interface.ts @@ -1,4 +1,3 @@ export interface IRemoveUserRequest { - tags: string[]; username: string; } diff --git a/src/modules/vision/vision.service.ts b/src/modules/vision/vision.service.ts index 91bd812..9ab4831 100644 --- a/src/modules/vision/vision.service.ts +++ b/src/modules/vision/vision.service.ts @@ -1,7 +1,9 @@ -import { InjectXtls } from '@remnawave/xtls-sdk-nestjs'; +import objectHash from 'object-hash'; + import { Injectable, Logger } from '@nestjs/common'; + +import { InjectXtls } from '@remnawave/xtls-sdk-nestjs'; import { XtlsApi } from '@remnawave/xtls-sdk'; -import objectHash from 'object-hash'; import { ICommandResponse } from '@common/types/command-response.type'; import { ERRORS } from '@libs/contracts/constants/errors'; diff --git a/src/modules/xray-core/xray.module.ts b/src/modules/xray-core/xray.module.ts index 7a48be2..34c29df 100644 --- a/src/modules/xray-core/xray.module.ts +++ b/src/modules/xray-core/xray.module.ts @@ -8,7 +8,7 @@ import { XrayService } from './xray.service'; imports: [InternalModule], providers: [XrayService], controllers: [XrayController], - exports: [], + exports: [XrayService], }) export class XrayModule implements OnModuleDestroy { constructor(private readonly xrayService: XrayService) {} diff --git a/src/modules/xray-core/xray.service.ts b/src/modules/xray-core/xray.service.ts index 93d3aa9..3adeee9 100644 --- a/src/modules/xray-core/xray.service.ts +++ b/src/modules/xray-core/xray.service.ts @@ -1,12 +1,16 @@ +import { ProcessInfo } from 'node-supervisord/dist/interfaces'; +import { SupervisordClient } from 'node-supervisord'; import { execa } from '@cjs-exporter/execa'; import { hasher } from 'node-object-hash'; import { table } from 'table'; import ems from 'enhanced-ms'; +import pRetry from 'p-retry'; import semver from 'semver'; import { Injectable, Logger, OnApplicationBootstrap, OnModuleInit } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { InjectSupervisord } from '@remnawave/supervisord-nestjs'; import { InjectXtls } from '@remnawave/xtls-sdk-nestjs'; import { XtlsApi } from '@remnawave/xtls-sdk'; @@ -23,6 +27,8 @@ import { } from './models'; import { InternalService } from '../internal/internal.service'; +const XRAY_PROCESS_NAME = 'xray' as const; + @Injectable() export class XrayService implements OnApplicationBootstrap, OnModuleInit { private readonly logger = new Logger(XrayService.name); @@ -35,9 +41,11 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { private isXrayOnline: boolean = false; private systemStats: ISystemStats | null = null; private isXrayStartedProccesing: boolean = false; + private xtlsConfigInbounds: Array = []; constructor( @InjectXtls() private readonly xtlsSdk: XtlsApi, + @InjectSupervisord() private readonly supervisordApi: SupervisordClient, private readonly internalService: InternalService, private readonly configService: ConfigService, ) { @@ -45,6 +53,7 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { this.xrayVersion = null; this.systemStats = null; this.isXrayStartedProccesing = false; + this.xtlsConfigInbounds = []; this.configEqualChecking = this.configService.getOrThrow('CONFIG_EQUAL_CHECKING'); } @@ -55,6 +64,8 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { async onApplicationBootstrap() { try { this.systemStats = await getSystemStats(); + + await this.supervisordApi.clearAllProcessLogs(); } catch (error) { this.logger.error(`Failed to get node hardware info: ${error}`); } @@ -86,6 +97,8 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { const fullConfig = generateApiConfig(config); + this.xtlsConfigInbounds = await this.extractInboundTags(fullConfig); + if (this.configEqualChecking) { this.logger.log('Getting config checksum...'); const newChecksum = this.getConfigChecksum(fullConfig); @@ -105,7 +118,7 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { `); if (oldChecksum === newChecksum && isXrayOnline) { - this.logger.error( + this.logger.warn( 'Xray is already online with the same config. Skipping...', ); @@ -126,22 +139,27 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { this.internalService.setXrayConfig(fullConfig); - this.logger.log(`XTLS config generated in ${performance.now() - tm}ms`); + this.logger.log( + 'XTLS config generated in: ' + + ems(performance.now() - tm, { + extends: 'short', + includeMs: true, + }), + ); - const xrayProcess = await execa('supervisorctl', ['restart', 'xray'], { - reject: false, - all: true, - cleanup: true, - timeout: 60_000, - lines: true, - }); + const xrayProcess = await this.restartXrayProcess(); - this.logger.debug(xrayProcess.all); + if (xrayProcess.error) { + this.logger.error(xrayProcess.error); + return { + isOk: false, + response: new StartXrayResponseModel(false, null, xrayProcess.error, null), + }; + } let isStarted = await this.getXrayInternalStatus(); - if (!isStarted && xrayProcess.all[1] === 'xray: started') { - await new Promise((resolve) => setTimeout(resolve, 2000)); + if (!isStarted && xrayProcess.processInfo!.state === 20) { isStarted = await this.getXrayInternalStatus(); } @@ -156,7 +174,7 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { ['Checksum', this.configChecksum], ['Master IP', ip], ['Internal Status', isStarted], - ['Error', xrayProcess.all.join(' | ')], + ['Error', xrayProcess.error], ], { header: { @@ -172,7 +190,7 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { response: new StartXrayResponseModel( isStarted, this.xrayVersion, - xrayProcess.all.join('\n'), + xrayProcess.error, this.systemStats, ), }; @@ -219,7 +237,13 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { response: new StartXrayResponseModel(false, null, errorMessage, null), }; } finally { - this.logger.log('Start XTLS took: ' + ems(performance.now() - tm, 'short')); + this.logger.log( + 'Start XTLS took: ' + + ems(performance.now() - tm, { + extends: 'short', + includeMs: true, + }), + ); this.isXrayStartedProccesing = false; } @@ -233,7 +257,6 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { this.configChecksum = null; this.internalService.setXrayConfig({}); - this.logger.log('Xray stopped due to request.'); return { isOk: true, response: new StopXrayResponseModel(true), @@ -290,7 +313,11 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { public async killAllXrayProcesses(): Promise { try { - await execa('supervisorctl', ['stop', 'xray'], { reject: false }); + try { + await this.supervisordApi.stopProcess(XRAY_PROCESS_NAME, true); + } catch (error) { + this.logger.error(`Response from supervisorctl stop: ${error}`); + } await execa('pkill', ['xray'], { reject: false }); @@ -309,13 +336,13 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { this.logger.log('Killed all Xray processes'); } catch (error) { - this.logger.log('No existing Xray processes found. Error: ', error); + this.logger.log(`No existing Xray processes found. Error: ${error}`); } } public async supervisorctlStop(): Promise { try { - await execa('supervisorctl', ['stop', 'xray'], { reject: false, timeout: 10_000 }); + await this.supervisordApi.stopProcess(XRAY_PROCESS_NAME, true); this.logger.log('Supervisorctl: XTLS stopped.'); } catch (error) { @@ -333,6 +360,7 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { private async getXrayVersionFromExec(): Promise { const output = await execa(this.xrayPath, ['version']); + const version = semver.valid(semver.coerce(output.stdout)); if (version) { @@ -362,30 +390,69 @@ export class XrayService implements OnApplicationBootstrap, OnModuleInit { } private async getXrayInternalStatus(): Promise { - const maxRetries = 8; - const delay = 2000; + try { + return await pRetry( + async () => { + const { isOk, message } = await this.xtlsSdk.stats.getSysStats(); - for (let attempt = 0; attempt < maxRetries; attempt++) { - try { - const { isOk } = await this.xtlsSdk.stats.getSysStats(); + if (!isOk) { + throw new Error(message); + } - if (isOk) { return true; - } + }, + { + retries: 10, + minTimeout: 2000, + maxTimeout: 2000, + onFailedAttempt: (error) => { + this.logger.debug( + `Get Xray internal status attempt ${error.attemptNumber} failed. ${error.retriesLeft} retries left.`, + ); + }, + }, + ); + } catch (error) { + this.logger.error(`Failed to get Xray internal status: ${error}`); + return false; + } + } - if (attempt < maxRetries - 1) { - this.logger.debug( - `Xray status check attempt ${attempt + 1} failed, retrying in ${delay}ms...`, - ); - await new Promise((resolve) => setTimeout(resolve, delay)); - } - } catch (error) { - this.logger.error(`Unexpected error during Xray status check: ${error}`); - return false; + private async restartXrayProcess(): Promise<{ + processInfo: ProcessInfo | null; + error: string | null; + }> { + try { + const processState = await this.supervisordApi.getProcessInfo(XRAY_PROCESS_NAME); + + // Reference: https://supervisord.org/subprocess.html#process-states + if (processState.state === 20) { + await this.supervisordApi.stopProcess(XRAY_PROCESS_NAME, true); } + + await this.supervisordApi.startProcess(XRAY_PROCESS_NAME, true); + + return { + processInfo: await this.supervisordApi.getProcessInfo(XRAY_PROCESS_NAME), + error: null, + }; + } catch (error) { + return { + processInfo: null, + error: error instanceof Error ? error.message : 'Unknown error', + }; } + } + + private async extractInboundTags(config: Record): Promise { + if (!config.inbounds || !Array.isArray(config.inbounds)) { + return []; + } + + return config.inbounds.map((inbound: { tag: string }) => inbound.tag); + } - this.logger.error(`Failed to get positive Xray status after ${maxRetries} attempts`); - return false; + public getSavedInboundsTags(): string[] { + return this.xtlsConfigInbounds; } } diff --git a/supervisord.conf b/supervisord.conf index 7ce5d9c..2e8c967 100644 --- a/supervisord.conf +++ b/supervisord.conf @@ -1,9 +1,3 @@ -[unix_http_server] -file=/var/run/supervisor.sock -chmod=0700 -username=remnawave -password=glcmYQLRwPXDXIBq - [supervisord] nodaemon=true user=root @@ -16,7 +10,12 @@ loglevel=info silent=true [supervisorctl] -serverurl=unix:///var/run/supervisor.sock +serverurl=127.0.0.1:61002 +username=remnawave +password=glcmYQLRwPXDXIBq + +[inet_http_server] +port=127.0.0.1:61002 username=remnawave password=glcmYQLRwPXDXIBq