From 34d0f082f58ce76877f621eb286304bc06a17a22 Mon Sep 17 00:00:00 2001 From: nguyentrungthat <80239428+nguentrungthat@users.noreply.github.com> Date: Mon, 10 Nov 2025 15:26:59 +0700 Subject: [PATCH] Update --- BACKEND/.env.example | 3 + BACKEND/adonisrc.ts | 3 +- BACKEND/app/services/line_connection.ts | 75 +++++++++---------- BACKEND/config/redis.ts | 36 ++++++++++ BACKEND/package-lock.json | 96 +++++++++++++++++++++++++ BACKEND/package.json | 1 + BACKEND/providers/socket_io_provider.ts | 35 +++++++-- BACKEND/start/env.ts | 6 +- FRONTEND/src/components/CardLine.tsx | 15 +++- 9 files changed, 218 insertions(+), 52 deletions(-) create mode 100644 BACKEND/config/redis.ts diff --git a/BACKEND/.env.example b/BACKEND/.env.example index 2bf7837..cd8a942 100644 --- a/BACKEND/.env.example +++ b/BACKEND/.env.example @@ -29,3 +29,6 @@ SEND_ZULIP=1 ZULIP_REALM="https://zulip.ipsupply.com.au" ZULIP_USERNAME="networktool-bot@zulip.ipsupply.com.au" ZULIP_API_KEY="0jMAmOuhfLvBqKJikv5oAkyNM4RIEoAM" +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 +REDIS_PASSWORD= \ No newline at end of file diff --git a/BACKEND/adonisrc.ts b/BACKEND/adonisrc.ts index 8b6e4c2..b74ce2e 100644 --- a/BACKEND/adonisrc.ts +++ b/BACKEND/adonisrc.ts @@ -47,7 +47,8 @@ export default defineConfig({ () => import('@adonisjs/cors/cors_provider'), () => import('@adonisjs/lucid/database_provider'), () => import('@adonisjs/auth/auth_provider'), - () => import('#providers/socket_io_provider') + () => import('#providers/socket_io_provider'), + () => import('@adonisjs/redis/redis_provider'), ], /* diff --git a/BACKEND/app/services/line_connection.ts b/BACKEND/app/services/line_connection.ts index b96a8bd..9702f03 100644 --- a/BACKEND/app/services/line_connection.ts +++ b/BACKEND/app/services/line_connection.ts @@ -1,5 +1,4 @@ import { textfsmResults } from './../ultils/templates/index.js' -import fs from 'node:fs' import net from 'node:net' import { appendLog, @@ -51,6 +50,7 @@ export default class LineConnection { private isRunningScript: boolean private connecting: boolean private waitingScenario: boolean + private outputInventory: string private outputScenario: string constructor(config: LineConfig, socketIO: any) { @@ -61,6 +61,7 @@ export default class LineConnection { this.isRunningScript = false this.connecting = false this.waitingScenario = false + this.outputInventory = '' this.outputScenario = '' } @@ -96,8 +97,9 @@ export default class LineConnection { if (this.isRunningScript) { this.waitingScenario = true this.outputBuffer += message + this.outputScenario += message if (!this.config.inventory) - this.outputScenario = this.outputScenario.slice(-3000) + message + this.outputInventory = this.outputInventory.slice(-3000) + message } if (message.includes('--More--')) this.writeCommand(' ') @@ -212,6 +214,7 @@ export default class LineConnection { this.isRunningScript = true const now = Date.now() + this.outputScenario += `\n\n---start-scenarios---${now}---\n---scenario---${script?.title}---${now}---\n` appendLog( `\n\n---start-scenarios---${now}---\n---scenario---${script?.title}---${now}---\n`, this.config.stationId, @@ -236,6 +239,7 @@ export default class LineConnection { lineId: this.config.id, data: 'Timeout run scenario', }) + this.outputScenario += `\n---end-scenarios---${now}---\n` appendLog( `\n---end-scenarios---${now}---\n`, this.config.stationId, @@ -256,59 +260,46 @@ export default class LineConnection { } else clearTimeout(timeoutTimer) this.isRunningScript = false this.outputBuffer = '' - this.outputScenario = '' + this.outputScenario += `\n---end-scenarios---${now}---\n` appendLog( `\n---end-scenarios---${now}---\n`, this.config.stationId, this.config.lineNumber, this.config.port ) - const pathLog = getPathLog( - this.config.stationId, - this.config.lineNumber, - this.config.port - ) - if (pathLog) - fs.readFile(pathLog, 'utf8', async (err, content) => { - if (err) return - - const logScenarios = getLogWithTimeScenario(content, now) || '' - this.socketIO.emit('output_test_scenario', { - data: logScenarios, - }) - const data = textfsmResults(logScenarios, '') - try { - data.forEach((item) => { - if (item?.textfsm && isValidJson(item?.textfsm)) { - if ( - ['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes( - item.command - ) - ) { - this.config.inventory = JSON.parse(item.textfsm)[0] - } - item.textfsm = JSON.parse(item.textfsm) - } - }) - this.config.data = data - this.socketIO.emit('data_textfsm', { - stationId: this.config.stationId, - lineId: this.config.id, - data, - inventory: this.config.inventory || null, - latestScenario: this.config.latestScenario || null, - }) - } catch (error) { - console.log(error) + const logScenarios = getLogWithTimeScenario(this.outputScenario, now) || '' + const data = textfsmResults(logScenarios, '') + try { + data.forEach((item) => { + if (item?.textfsm && isValidJson(item?.textfsm)) { + if ( + ['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command) + ) { + this.config.inventory = JSON.parse(item.textfsm)[0] + } + item.textfsm = JSON.parse(item.textfsm) } }) + this.config.data = data + this.socketIO.emit('data_textfsm', { + stationId: this.config.stationId, + lineId: this.config.id, + data, + inventory: this.config.inventory || null, + latestScenario: this.config.latestScenario || null, + }) + } catch (error) { + console.log(error) + } + this.outputScenario = '' resolve(true) return } const step = steps[index] + this.outputScenario += `\n---send-command---"${step?.send ?? ''}"---${now}---\n` appendLog( `\n---send-command---"${step?.send ?? ''}"---${now}---\n`, this.config.stationId, @@ -474,7 +465,7 @@ export default class LineConnection { } getInventory = () => { - const data = textfsmResults(this.outputScenario, 'show inventory') + const data = textfsmResults(this.outputInventory, 'show inventory') try { data.forEach((item) => { if (item?.textfsm && isValidJson(item?.textfsm)) { @@ -493,7 +484,7 @@ export default class LineConnection { inventory: this.config.inventory || null, latestScenario: this.config.latestScenario || null, }) - this.outputScenario = '' + this.outputInventory = '' } } catch (error) { console.log(error) diff --git a/BACKEND/config/redis.ts b/BACKEND/config/redis.ts new file mode 100644 index 0000000..f287ab0 --- /dev/null +++ b/BACKEND/config/redis.ts @@ -0,0 +1,36 @@ +import env from '#start/env' +import { defineConfig } from '@adonisjs/redis' +import { InferConnections } from '@adonisjs/redis/types' + +const redisConfig = defineConfig({ + connection: 'main', + + connections: { + /* + |-------------------------------------------------------------------------- + | The default connection + |-------------------------------------------------------------------------- + | + | The main connection you want to use to execute redis commands. The same + | connection will be used by the session provider, if you rely on the + | redis driver. + | + */ + main: { + host: env.get('REDIS_HOST'), + port: env.get('REDIS_PORT'), + password: env.get('REDIS_PASSWORD', ''), + db: 0, + keyPrefix: '', + retryStrategy(times) { + return times > 10 ? null : times * 50 + }, + }, + }, +}) + +export default redisConfig + +declare module '@adonisjs/redis/types' { + export interface RedisConnections extends InferConnections {} +} diff --git a/BACKEND/package-lock.json b/BACKEND/package-lock.json index ae50281..a97f745 100644 --- a/BACKEND/package-lock.json +++ b/BACKEND/package-lock.json @@ -13,6 +13,7 @@ "@adonisjs/core": "^6.18.0", "@adonisjs/cors": "^2.2.1", "@adonisjs/lucid": "^21.6.1", + "@adonisjs/redis": "^9.2.0", "@vinejs/vine": "^3.0.1", "axios": "^1.13.2", "luxon": "^3.7.2", @@ -582,6 +583,23 @@ "prettier-plugin-edgejs": "^1.0.1" } }, + "node_modules/@adonisjs/redis": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@adonisjs/redis/-/redis-9.2.0.tgz", + "integrity": "sha512-DUI9NrHDLZ2ISNjMlqWbKJT99ZYj1ZmvhNFTfhVs9lc7K2KJmNKZfK8Y85a8eN7q+ZYMBYSu1uRemxGs6xRaYw==", + "license": "MIT", + "dependencies": { + "@poppinss/utils": "^6.9.2", + "emittery": "^1.1.0", + "ioredis": "^5.4.2" + }, + "engines": { + "node": ">=20.6.0" + }, + "peerDependencies": { + "@adonisjs/core": "^6.2.0" + } + }, "node_modules/@adonisjs/repl": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/@adonisjs/repl/-/repl-4.1.2.tgz", @@ -999,6 +1017,12 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@ioredis/commands": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", + "license": "MIT" + }, "node_modules/@japa/api-client": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@japa/api-client/-/api-client-3.1.0.tgz", @@ -2971,6 +2995,15 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/code-block-writer": { "version": "13.0.3", "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.3.tgz", @@ -4719,6 +4752,30 @@ "node": ">= 0.10" } }, + "node_modules/ioredis": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", + "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.4.0", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -5117,6 +5174,18 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6299,6 +6368,27 @@ "node": ">= 10.13.0" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -6878,6 +6968,12 @@ "get-source": "^2.0.12" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/BACKEND/package.json b/BACKEND/package.json index e7bbe98..a006280 100644 --- a/BACKEND/package.json +++ b/BACKEND/package.json @@ -55,6 +55,7 @@ "@adonisjs/core": "^6.18.0", "@adonisjs/cors": "^2.2.1", "@adonisjs/lucid": "^21.6.1", + "@adonisjs/redis": "^9.2.0", "@vinejs/vine": "^3.0.1", "axios": "^1.13.2", "luxon": "^3.7.2", diff --git a/BACKEND/providers/socket_io_provider.ts b/BACKEND/providers/socket_io_provider.ts index 7a31d54..64af888 100644 --- a/BACKEND/providers/socket_io_provider.ts +++ b/BACKEND/providers/socket_io_provider.ts @@ -11,6 +11,7 @@ import Station from '#models/station' import APCController from '#services/apc_connection' import { sleep } from '../app/ultils/helper.js' import SwitchController from '#services/switch_connection' +import redis from '@adonisjs/redis/services/main' interface HandleOptions { command?: string @@ -64,17 +65,17 @@ export class WebSocketIo { intervalMap: { [key: string]: NodeJS.Timeout } = {} stationMap: Map = new Map() lineMap: Map = new Map() // key = lineId - lineConnecting: number[] = [] // key = lineId userConnecting: Map = new Map() apcsControl: Map = new Map() switchControl: Map = new Map() + lineConnecting: number[] = [] // key = lineId constructor(protected app: ApplicationService) {} async boot() { const SOCKET_IO_PORT = env.get('SOCKET_PORT') || 8989 const FRONTEND_URL = env.get('FRONTEND_URL') || 'http://localhost:5173' - + await this.restoreState() const socketServer = http.createServer() const io = new SocketIOServer(socketServer, { pingInterval: 25000, // 25s server gửi ping @@ -103,16 +104,16 @@ export class WebSocketIo { setTimeout(() => { io.to(socket.id).emit( 'init', - Array.from(this.lineMap.values()).map((el) => el.config) + Array.from(this.lineMap.values()).map((el) => el?.config || {}) ) }, 500) socket.on('disconnect', () => { console.log(`FE disconnected: ${socket.id}`) this.userConnecting.delete(userId) - const listLineS = Array.from(this.lineMap.values()).map((el) => el.config) + const listLineS = Array.from(this.lineMap.values()).map((el) => el?.config || {}) listLineS.forEach((el) => { - if (el.userOpenCLI === userName) { + if (el?.userOpenCLI === userName) { const line = this.lineMap.get(el.id) if (line) line.userCloseCLI() } @@ -415,6 +416,9 @@ export class WebSocketIo { console.log(`Socket server is running on port ${SOCKET_IO_PORT}`) }) + // 🔹 Tự động lưu dữ liệu định kỳ mỗi 10 giây + setInterval(async () => await this.saveState(), 10000) + return io } @@ -644,4 +648,25 @@ export class WebSocketIo { }) }) } + + async saveState() { + const newMap = new Map() + this.lineMap.forEach((line, id) => { + if (line && line.config) { + newMap.set(id, { config: { ...line.config, status: 'disconnected' } } as LineConnection) + } + }) + + const data = { + lineMap: newMap ? [...newMap.entries()] : [], + } + await redis.set('socket_state', JSON.stringify(data)) + } + + async restoreState() { + const raw = await redis.get('socket_state') + if (!raw) return + const parsed = JSON.parse(raw) + this.lineMap = new Map(parsed.lineMap) + } } diff --git a/BACKEND/start/env.ts b/BACKEND/start/env.ts index 9717b6f..5766109 100644 --- a/BACKEND/start/env.ts +++ b/BACKEND/start/env.ts @@ -27,5 +27,9 @@ export default await Env.create(new URL('../', import.meta.url), { DB_PORT: Env.schema.number(), DB_USER: Env.schema.string(), DB_PASSWORD: Env.schema.string.optional(), - DB_DATABASE: Env.schema.string() + DB_DATABASE: Env.schema.string(), + + REDIS_HOST: Env.schema.string({ format: 'host' }), + REDIS_PORT: Env.schema.number(), + REDIS_PASSWORD: Env.schema.string.optional() }) diff --git a/FRONTEND/src/components/CardLine.tsx b/FRONTEND/src/components/CardLine.tsx index aa2964b..13d937c 100644 --- a/FRONTEND/src/components/CardLine.tsx +++ b/FRONTEND/src/components/CardLine.tsx @@ -86,13 +86,22 @@ const CardLine = ({
- PID: {line?.inventory?.pid || ""} + PID:{" "} + + {line?.inventory?.pid || ""} +
- SN: {line?.inventory?.sn || ""} + SN:{" "} + + {line?.inventory?.sn || ""} +
- VID: {line?.inventory?.vid || ""} + VID:{" "} + + {line?.inventory?.vid || ""} +