import moment from 'moment' import momentTZ from 'moment-timezone' import net from 'node:net' import fs from 'node:fs' import { Server as SocketIOServer } from 'socket.io' import http from 'node:http' import LineConnection from '../app/services/line_connection.js' import { ApplicationService } from '@adonisjs/core/types' import env from '#start/env' import { CustomServer, CustomSocket } from '../app/ultils/types.js' import Line from '#models/line' import Station from '#models/station' import APCController from '#services/apc_connection' import { appendLog, checkStationActive, cleanData, sendMessageToMail, sendMessageToZulip, sleep, } from '../app/ultils/helper.js' import SwitchController from '#services/switch_connection' import redis from '@adonisjs/redis/services/main' import axios from 'axios' import StationConnection from '#services/station_connection' import Scenario from '#models/scenario' interface HandleOptions { command?: string actionApc?: string scenario?: any timeout?: number baud?: number } interface HistoryItem { pid: string vid: string sn: string scenario: string id: number number: number stationId: number timestamp?: number } type LineAction = (line: LineConnection, options?: HandleOptions) => Promise | void type StationAction = ( line: StationConnection, options?: HandleOptions ) => Promise | void export default class SocketIoProvider { private static _io: CustomServer constructor(protected app: ApplicationService) { } /** * Register bindings to the container */ register() { } /** * The container bindings have booted */ async boot() { } /** * The application has been booted */ async start() { } /** * The process has been started */ async ready() { if (process.argv[1].includes('server.js')) { const webSocket = new WebSocketIo(this.app) SocketIoProvider._io = await webSocket.boot() } } /** * Preparing to shutdown the app */ async shutdown() { } public static get io() { return this._io } } export class WebSocketIo { intervalMap: { [key: string]: NodeJS.Timeout } = {} stationMap: Map = new Map() // key = stationId lineMap: Map = new Map() // key = lineId userConnecting: Map = new Map() apcsControl: Map = new Map() switchControl: Map = new Map() lineConnecting: number[] = [] // key = lineId intervalKeepConnect: { [key: string]: NodeJS.Timeout } = {} 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 pingTimeout: 20000, // chờ 20s không có pong thì disconnect cors: { origin: [FRONTEND_URL], methods: ['GET', 'POST'], credentials: true, }, }) io.on('connection', (socket: CustomSocket) => { const { userId, userName } = socket.handshake.auth console.log('Socket connected:', socket.id) socket.connectionTime = new Date() this.userConnecting.set(userId, { userId, userName }) setTimeout(() => { const listUser = Array.from(this.userConnecting.values()) if (!listUser.find((el) => el.userId === userId)) { listUser.push({ userId, userName }) } io.emit('user_connecting', listUser) }, 500) setTimeout(() => { const listLine = Array.from(this.lineMap.values()) io.to(socket.id).emit( 'init', listLine.map((el) => { const config = el?.config || {} if (config.status !== 'connected') { config.runningScenario = '' config.runningPhysical = false config.output = config.output + '[CLEAR_TERMINAL_SCROLL_BACK]' config.listFeatureTested = [] } return config }) ) }, 500) socket.on('disconnect', () => { console.log(`FE disconnected: ${socket.id}`) this.userConnecting.delete(userId) const listLine = Array.from(this.lineMap.values()).map((el) => el?.config || {}) listLine.forEach((el) => { if (el?.userOpenCLI === userName) { const line = this.lineMap.get(el.id) if (line) { line.config.openCLI = false line.config.userEmailOpenCLI = '' line.config.userOpenCLI = '' io.emit('user_close_cli', { stationId: line.config.stationId, lineId: line.config.id, userEmailOpenCLI: '', }) } } }) setTimeout(() => { io.emit('user_connecting', Array.from(this.userConnecting.values())) }, 200) }) // FE gửi yêu cầu connect lines socket.on('connect_lines', async (data) => { const { stationData, linesData } = data // Check station is active const activeStation = await checkStationActive(stationData.id) if (!activeStation) return await this.connectLine(io, linesData, stationData) }) socket.on('write_command_line_from_web', async (data) => { const { lineIds, stationId, command } = data // Check station is active // const activeStation = await checkStationActive(stationId) // if (!activeStation) return await this.handleLineOperation( io, stationId, lineIds, async (line) => command === 'spam_break' ? line.breakSpam() : line.writeCommand(command), { command } ) }) socket.on('run_scenario', async (data) => { const lineId = data.id const scenario = data.scenario const name = data.userName || userName // Check station is active const activeStation = await checkStationActive(data.stationId) if (!activeStation) return await this.handleLineOperation( io, data.stationId, [lineId], async (line) => line.runScript(scenario, name), { scenario, } ) }) socket.on('set_baud', async (data) => { console.log('Set baud', data) const lineId = data.lineId const baud = data.baud const line = await Line.find(lineId) // Check station is active const activeStation = await checkStationActive(data.stationId) if (!activeStation) return if (!line) { console.log(`Line [${lineId}] not found!!!`) return } Object.assign(line, { baud }) line?.save() await this.setBaudByClearLine(data.stationId, line?.lineClear, baud, lineId, io) }) socket.on('open_cli', async (data) => { const { lineId, userEmail, userName: name, stationId } = data const line = this.lineMap.get(lineId) if (line) { if (line?.userOpenCLI) line?.userOpenCLI({ userEmail, userName: name }) else { line.config.openCLI = true line.config.userEmailOpenCLI = userEmail line.config.userOpenCLI = userName io.emit('user_open_cli', { stationId: line.config.stationId, lineId: line.config.id, userEmailOpenCLI: userEmail, userOpenCLI: userName, }) } if (line.config?.status !== 'connected') await this.handleLineOperation( io, stationId, [lineId], async (lineCon) => lineCon.writeCommand('\r\n'), { command: '\r\n' } ) } else { if (this.lineConnecting.includes(lineId)) return const linesData = await Line.findBy('id', lineId) const stationData = await Station.findBy('id', stationId) if (linesData && stationData) { this.lineConnecting.push(lineId) await this.connectLine(io, [linesData], stationData) const lineReconnect = this.lineMap.get(lineId) if (lineReconnect) { lineReconnect.userOpenCLI({ userEmail, userName: name }) } } } }) socket.on('close_cli', async (data) => { const { lineId, stationId } = data const line = this.lineMap.get(lineId) if (line) { if (line?.userCloseCLI) line?.userCloseCLI() else { line.config.openCLI = false line.config.userEmailOpenCLI = '' line.config.userOpenCLI = '' io.emit('user_close_cli', { stationId: line.config.stationId, lineId: line.config.id, userEmailOpenCLI: '', }) } } else { if (this.lineConnecting.includes(lineId)) return const linesData = await Line.findBy('id', lineId) const stationData = await Station.findBy('id', stationId) if (linesData && stationData) { this.lineConnecting.push(lineId) await this.connectLine(io, [linesData], stationData) const lineReconnect = this.lineMap.get(lineId) if (lineReconnect) { lineReconnect.userCloseCLI() } } } }) socket.on('request_take_over', async (data) => { io.emit('confirm_take_over', data) }) socket.on('get_list_logs', async (data) => { const dir = 'storage/system_logs' let getListSystemLogs = fs .readdirSync(dir) .map((file) => { const fullPath = `${dir}/${file}` const stat = fs.statSync(fullPath) return { path: fullPath, time: stat.mtime.getTime(), // hoặc stat.birthtime } }) .sort((a, b) => b.time - a.time) // giảm dần (mới nhất trước) .slice(0, 1000) .map((f) => f.path) // lấy lại path io.to(socket.id).emit('list_logs', getListSystemLogs) // const listHistory = await this.getHistory(data?.stationId, data?.lineId) // io.to(socket.id).emit('list_histories', listHistory) }) socket.on('get_content_log', async (data) => { try { const { line, socketId } = data const filePath = `storage/system_logs/${line.systemLogUrl}` if (fs.existsSync(filePath)) { // Get file stats const stats = fs.statSync(filePath) const fileSizeInBytes = stats.size if (fileSizeInBytes / 1024 / 1024 > 0.5) { // File is larger than 0.5 MB const fileId = Date.now() // Mã định danh file const chunkSize = 64 * 1024 // 64KB const fileBuffer = fs.readFileSync(filePath) const totalChunks = Math.ceil(fileBuffer.length / chunkSize) for (let i = 0; i < totalChunks; i++) { const chunk = fileBuffer.slice(i * chunkSize, (i + 1) * chunkSize) io.to(socketId).emit('response_content_log', { type: 'chunk', chunk: { fileId, chunkIndex: i, totalChunks, chunk, }, }) } } else { console.log(filePath) const content = fs.readFileSync(filePath) socket.emit('response_content_log', content) } } else { io.to(socketId).emit('response_content_log', Buffer.from('File not found', 'utf-8')) } } catch (error) { console.log(error) } }) socket.on('control_apc', async (data) => { try { const { outletNumbers, action, station, apcName } = data // Check station is active const activeStation = await checkStationActive(station.id) if (!activeStation) return if (action === 'reconnect') { if (!station) return const apcIp = (station as any)[`${apcName}_ip`] as string const apc = this.apcsControl.get(apcIp) if (apc) { await apc.reconnect() this.keepConnectAPC(apcIp, io) } else await this.connectApc(io, apcName, station) } else { for (const outletNumber of outletNumbers) { if (!outletNumber || outletNumber < 0) return if (!station) return // find line from station by apcName and outletNumber const lines = await Line.query() .where('station_id', station.id) .andWhere('apc_name', apcName) .andWhere('outlet', outletNumber) if (lines.length > 0) { const line = this.lineMap.get(lines[0].id) if (line) this.setTimeoutConnect(lines[0].id, line) } const apcIp = (station as any)[`${apcName}_ip`] as string if (!this.apcsControl.get(apcIp)) await this.connectApc(io, apcName, station) const apc = this.apcsControl.get(apcIp) if (apc && apc.status !== 'CONNECTED') { await apc.reconnect() this.keepConnectAPC(apcIp, io) } if (apc) { switch (action) { case 'on': await apc?.turnOnOutlet(outletNumber) break case 'off': await apc?.turnOffOutlet(outletNumber) break case 'restart': await apc?.restartOutlet(outletNumber) break case 'reconnect': await apc?.reconnect() break default: break } setTimeout(() => { apc?.navigateToOutlets() }, 10000) } } } } catch (e) { console.log('control_apc', e) } }) socket.on('connect_apc', async (data) => { try { const { apcIp, station, apcName } = data // Check station is active const activeStation = await checkStationActive(station.id) if (!activeStation) return const apc = this.apcsControl.get(apcIp) if (apc && apc.status === 'CONNECTED') { socket.emit('apc_output', { stationId: station.id, apcNumber: apcName === 'apc_1' ? 1 : 2, data: apc.output, status: apc.status, }) this.keepConnectAPC(apcIp, io) } else if (apc && apc.status !== 'CONNECTED') { await apc.reconnect() this.apcsControl.set(apcIp, apc) this.keepConnectAPC(apcIp, io) } else await this.connectApc(io, apcName, station) } catch (error) { console.log(error) } }) socket.on('connect_switch', async (data) => { try { const { ip, station } = data // Check station is active const activeStation = await checkStationActive(station.id) if (!activeStation) return const element = this.switchControl.get(ip) // Connect station if not connected const stationData = await Station.findBy('id', station.id) if (stationData) await this.connectStation(stationData) if (element && element.status === 'CONNECTED') { socket.emit('switch_output', { stationId: station.id, portGroups: element.portGroups, status: element.status, }) socket.emit('switch_ports_status', { stationId: station.id, ports: Array.isArray(element.portGroups) && element.portGroups?.length > 0 ? element.portGroups?.flat() : [], }) } else if (element && element.status !== 'CONNECTED') { await element.reconnect() } else await this.connectSwitch(io, station) } catch (error) { console.log(error) } }) socket.on('control_switch', async (data) => { const { ports, command, ip, station } = data // Check station is active const activeStation = await checkStationActive(station.id) if (!activeStation) return const element1 = this.switchControl.get(ip) if (!element1) { await this.connectSwitch(io, station) } const element = this.switchControl.get(ip) if (element) { this.setTimeoutConnect(station.id, element) switch (command) { case 'on': ports.forEach(async (port: string) => { console.log(`Turn on port ${port}`) await element?.turnPortOn(port) await sleep(500) }) break case 'off': ports.forEach(async (port: string) => { console.log(`Turn off port ${port}`) await element?.turnPortOff(port) await sleep(500) }) break case 'restart': ports.forEach(async (port: string) => { console.log(`Restarting on port ${port}`) await element?.restartPort(port) await sleep(500) }) break case 'on-poe': ports.forEach(async (port: string) => { console.log(`Turn on port poe ${port}`) await element?.enablePoE(port) await sleep(500) }) break case 'off-poe': ports.forEach(async (port: string) => { console.log(`Turn off port poe ${port}`) await element?.disablePoE(port) await sleep(500) }) break case 'restart-poe': ports.forEach(async (port: string) => { console.log(`Restarting on port poe ${port}`) await element?.restartPoE(port) await sleep(500) }) break case 'reconnect': await element?.reconnect() break default: break } } }) socket.on('update_ticket', async (data) => { io.emit('update_ticket', data) }) socket.on('update_line_value', async (data) => { const { lineId, update } = data const line = this.lineMap.get(lineId) if (line) { line.config = { ...line.config, ...update } } }) socket.on('clear_line', async (data) => { const { stationId, lineClear } = data await this.clearLineBeforeConnect(stationId, lineClear) }) socket.on('get_line_history', async (data) => { const stationId = Number(data?.stationId) const lineId = Number(data?.lineId) if (!stationId || !lineId) { return io.to(socket.id).emit('line_history', { stationId, lineId, history: [] }) } const history = await this.getHistory(stationId, lineId) io.to(socket.id).emit('line_history', { stationId, lineId, history }) }) socket.on('get_list_history', async (data) => { const { stationIds = [] } = data if (!Array.isArray(stationIds) || stationIds.length === 0) { return io.to(socket.id).emit('list_histories', []) } // Xử lý song song tất cả stationId const stationPromises = stationIds.map(async (stationId) => { // Lấy station + preload lines const station = await Station.query().where('id', stationId).preload('lines').first() if (!station) { return { stationId, stationName: null, history: [], } } // Lấy history cho mỗi line const linePromises = station.lines.map((line) => this.getHistory(station.id, line.id)) const list = await Promise.all(linePromises) const mergedHistory = list.flat() return { stationId: station.id, stationName: station.name, history: mergedHistory, } }) const result = await Promise.all(stationPromises) io.to(socket.id).emit('list_histories', result) }) socket.on('run_all_dpelp', async (data) => { try { console.log('[DPELP] Received run all dpelp', data) const dataLines = data.lineIds || [] const stationName = data.stationName || '' const stationId = data.stationId || '' const scenarioName = data.scenarioName || '' const skipTestPorts = data.skipTestPorts || false const reasonSkipPhysical = data.reasonSkipPhysical || false const station = await Station.find(stationId) // Check station is active const activeStation = await checkStationActive(stationId) if (!activeStation) return // Filter line is not ready let lineIds: number[] = [] for (const lineId of dataLines) { const line = this.lineMap.get(lineId) if (line && line.config.isReady) { lineIds.push(lineId) if (skipTestPorts) { line.config.listFeatureTested = [ ...new Set([...line.config.listFeatureTested, 'PHYSICAL']), ] line.config.isSkipPhysical = true line.config.reasonSkipPhysical = reasonSkipPhysical } else if (line.config.status === 'connected') { console.log('Reset list feature tested for line', lineId) line.resetDPELP() } } } // Check station sendWiki flag // console.log('[DPELP] Received run all dpelp', lineIds) if (!station || !station?.send_wiki || lineIds?.length < 1) return const results = await this.waitUntilAllReady(lineIds) const tableHTML = this.generateTable(results) const zulipMess = this.generateZulipMessage(results) const timeZone = process.env.TIME_ZONE || 'Australia/Sydney' const dataFormat = momentTZ().tz(timeZone).format('YYYY/MM/DD, HH:mm:ss') const streamZulip = station.name.toUpperCase().includes('US') ? process.env.ZULIP_STREAM_US : process.env.ZULIP_STREAM_AU const topicZulip = station.name.toUpperCase().includes('US') ? '' : process.env.ZULIP_TOPIC_AU const linkWiki = process.env.LINK_WIKI || 'https://logs.danielvu.com/api/wiki/page/insert?title=Dev_test' try { await axios.post(linkWiki, { data: tableHTML, titleAuto: `[${scenarioName || 'DPELP'}] - ${stationName} - ` + dataFormat, }) } catch (error) { console.error('Error sending wiki message:', error) } try { // await sendMessageToMail( // `[${scenarioName || 'DPELP'}] - ${stationName} - ${dataFormat}`, // tableHTML // ) } catch (error) { console.error('Error sending mail:', error) } try { const contentZulip = `\n\n---\n**[${scenarioName || 'DPELP'}] - ${stationName} - ${dataFormat}**\n\n` + zulipMess await sendMessageToZulip( 'stream', streamZulip || 'ATC_Report', topicZulip, contentZulip ) await sendMessageToZulip('stream', 'ATC_Report', station.name, contentZulip) } catch (error) { console.error('Error sending zulip message:', error) } } catch (error) { console.error(error) } }) socket.on('clear_terminal', async (data) => { const { stationId, lineId } = data // Check station is active const activeStation = await checkStationActive(stationId) if (!activeStation) return await this.handleLineOperation( io, stationId, [lineId], async (lineCon) => lineCon.clearCLI(), {} ) }) socket.on('run_physical_test', async (data) => { const { stationId, lineId, userName: name } = data // Check station is active const activeStation = await checkStationActive(stationId) if (!activeStation) return await this.handleLineOperation( io, stationId, [lineId], async (lineCon) => lineCon.runPhysicalTest(name || userName), {} ) }) socket.on('end_run_physical_test', async (data) => { const { stationId, lineId, reasonSkipPhysical } = data // Check station is active const activeStation = await checkStationActive(stationId) if (!activeStation) return await this.handleLineOperation( io, stationId, [lineId], async (lineCon) => { await lineCon.sendReportPhysicalTest(reasonSkipPhysical) lineCon.endTesting() }, {} ) }) socket.on('reset_physical_test', async (data) => { const { stationId, lineId } = data // Check station is active const activeStation = await checkStationActive(stationId) if (!activeStation) return await this.handleLineOperation( io, stationId, [lineId], async (lineCon) => { lineCon.physicalTest.resetTestedPorts() io.emit('running_scenario', { stationId: stationId, lineId: lineId, title: 'Physical Test', physical: true, ports: lineCon.config.ports, }) }, {} ) }) socket.on('load_ios_router', async (data) => { const { stationId, lineId, iosName, outletNumber, station, apcName, isReboot } = data // Check station is active const activeStation = await checkStationActive(stationId) if (!activeStation) return await this.handleLineOperation( io, stationId, [lineId], async (lineCon) => { if (isReboot) { const network = station?.gateway || '172.25.1.1' const tftpIp = station?.tftp_ip || '172.16.7.69' const [a, b] = network.split('.').map(Number) const address = `${a}.${b}.100.${lineCon.config.id < 254 ? lineCon.config.id : 254 - lineCon.config.id}` const gateway = `${station?.gateway ? station?.gateway : '0.0.0.0'}` await lineCon.configAddressGateway(address, gateway, 'GigabitEthernet0/0', true) const pingSuccess = await lineCon.pingToServer(tftpIp) if (!pingSuccess) return await lineCon.backupIos(iosName) if (!outletNumber || outletNumber < 0) return if (!station) return const apcIp = (station as any)[`${apcName}_ip`] as string if (!this.apcsControl.get(apcIp)) await this.connectApc(io, apcName, station) const apc = this.apcsControl.get(apcIp) if (apc && apc.status !== 'CONNECTED') { await apc.reconnect() this.keepConnectAPC(apcIp, io) } if (apc) { await apc?.restartOutlet(outletNumber) setTimeout(() => { apc?.navigateToOutlets() }, 10000) } } await lineCon.loadIosRouter(iosName, userName) }, {} ) }) socket.on('load_ios_switch', async (data) => { const { stationId, lineId, iosName } = data // Check station is active const activeStation = await checkStationActive(stationId) if (!activeStation) return await this.handleLineOperation( io, stationId, [lineId], async (lineCon) => { lineCon.loadIosSwitch(iosName, userName) }, {} ) }) socket.on('load_license_router', async (data) => { const { stationId, lineId, licenseName, portName } = data // Check station is active const activeStation = await checkStationActive(stationId) if (!activeStation) return await this.handleLineOperation( io, stationId, [lineId], async (lineCon) => { lineCon.loadLicenseRouter(licenseName, userName, portName) }, {} ) }) socket.on('load_license_switch', async (data) => { const { stationId, lineId, licenseName, portName } = data // Check station is active const activeStation = await checkStationActive(stationId) if (!activeStation) return await this.handleLineOperation( io, stationId, [lineId], async (lineCon) => { lineCon.loadLicenseSwitch(licenseName, userName, portName) }, {} ) }) socket.on('update_note', async (data) => { console.log('Update note', data) const { stationId, lineId, note, licenses, sn } = data // Check station is active const activeStation = await checkStationActive(stationId) if (!activeStation) return await this.handleLineOperation( io, stationId, [lineId], async (lineCon) => { lineCon.updateNoteFromUser(sn, note, licenses) }, {} ) }) socket.on('send_summary_report', async (data) => { const { stationId, lineId } = data // Check station is active const activeStation = await checkStationActive(stationId) if (!activeStation) return await this.handleLineOperation( io, stationId, [lineId], async (lineCon) => { lineCon.sendReportSummaryV2() }, {} ) }) }) socketServer.listen(SOCKET_IO_PORT, () => { 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 } private async connectLine( socket: any, lines: Line[], station: Station, output = '', inventory: string = '', latestScenario?: any, data?: any, reasonSkipPhysical?: string ) { try { for (const line of lines) { const lineConn = new LineConnection( { id: line.id, port: line.port, ip: station.ip, lineNumber: line.lineNumber, stationId: station.id, stationName: station.name, stationIp: station.ip, apcName: line.apcName, outlet: line.outlet, baud: line.baud, output: output, status: '', openCLI: false, userEmailOpenCLI: '', userOpenCLI: '', data: data, ports: [], inventory: inventory, runningPhysical: false, runningScenario: '', latestScenario: latestScenario, listFeatureTested: [], isReady: false, reasonSkipPhysical: reasonSkipPhysical, isSkipPhysical: reasonSkipPhysical ? true : false, }, socket, async () => { if (line.lineClear && line.lineClear > 0) await this.clearLineBeforeConnect(station.id, line.lineClear) } ) // 👉 Bước 1: clear line trước khi connect if (line.lineClear && line.lineClear > 0) await this.clearLineBeforeConnect(station.id, line.lineClear) await sleep(500) this.lineMap.set(line.id, lineConn) await lineConn.connect() lineConn.writeCommand('\r\n\r\n') this.setTimeoutConnect(line.id, lineConn) if (line.lineClear && line.lineClear > 0) await this.checkBaudByClearLine(station.id, line.lineClear, line.id, socket) } } catch (error) { console.log(error) } } private setTimeoutConnect = ( lineId: number, lineConn: LineConnection | SwitchController | StationConnection, timeout = 28800000 // 8h = 8*60*60*1000 ) => { if (this.intervalMap[`${lineId}`]) { clearInterval(this.intervalMap[`${lineId}`]) delete this.intervalMap[`${lineId}`] } const interval = setInterval(() => { if (lineConn.disconnect) lineConn.disconnect() // this.lineMap.delete(lineId) if (this.intervalMap[`${lineId}`]) { clearInterval(this.intervalMap[`${lineId}`]) delete this.intervalMap[`${lineId}`] } if (this.intervalKeepConnect[`${lineId}`]) { clearInterval(this.intervalKeepConnect[`${lineId}`]) delete this.intervalKeepConnect[`${lineId}`] } }, timeout) this.intervalMap[`${lineId}`] = interval } /** * Hàm xử lý chung cho mọi action (write command, runScript, v.v.) */ async handleLineOperation( io: CustomServer, stationId: number, lineIds: number[], action: LineAction, options: HandleOptions = {} ): Promise { lineIds.forEach(async (lineId) => { try { const line = this.lineMap.get(lineId) // console.log(line?.config) if (line && line.config.status === 'connected') { this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId) this.setTimeoutConnect(lineId, line) // await sleep(500) await action(line, options) } else { if (!this.lineConnecting.includes(lineId)) { const linesData = await Line.findBy('id', lineId) const stationData = await Station.findBy('id', stationId) io.emit('line_connecting', { stationId, lineId, }) if (linesData && stationData) { this.lineConnecting.push(lineId) await this.connectLine( io, [linesData], stationData, line?.config?.output || '', line?.config?.inventory || '', line?.config?.latestScenario || undefined, line?.config?.data || [], line?.config?.reasonSkipPhysical || '' ) this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId) const lineReconnect = this.lineMap.get(lineId) if (lineReconnect) { this.setTimeoutConnect(lineId, lineReconnect) await sleep(100) await action(lineReconnect, options) } } else { io.emit('line_disconnected', { stationId, lineId, status: 'disconnected', }) io.emit('line_error', { lineId, error: 'Line not connected\r\n', stationId }) } } } } catch (err: any) { this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId) io.emit('line_error', { lineId, error: `\n[ERROR] ${err.message}\n`, stationId }) } }) } private async connectApc(socket: any, apcName: string, station: Station) { try { const ip = (station as any)[`${apcName}_ip`] as string const port = (station as any)[`${apcName}_port`] as number const username = (station as any)[`${apcName}_username`] as string const password = (station as any)[`${apcName}_password`] as string if (!ip || !port || !username || !password) throw new Error(`Missing APC configuration for ${apcName}`) // Tạo APC Controller instance const apc = new APCController({ host: ip, port, username, password, stationId: station.id, stationName: station.name, stationIP: station.ip, number: apcName === 'apc_1' ? 1 : 2, onData: (data: string, status: string) => { socket.emit('apc_output', { stationId: station.id, apcNumber: apcName === 'apc_1' ? 1 : 2, data, status, }) }, }) // Connect và login await apc.connect() await apc.login() this.apcsControl.set(ip, apc) this.keepConnectAPC(ip, socket) } catch (error) { console.log(error) } } private async connectSwitch(socket: any, station: Station) { try { const ip = station.switch_control_ip as string const port = station.switch_control_port as number const username = (station.switch_control_username as string) || '' const password = (station.switch_control_password as string) || '' if (!ip || !port) { socket.emit('switch_output', { stationId: station.id, portGroups: [], status: 'DISCONNECTED', message: `Missing Switch configuration`, }) return } // Tạo APC Controller instance const infoSwitch = new SwitchController({ host: ip, port: port, username: username, password: password, stationId: station.id, stationName: station.name, stationIP: station.ip, onData: (ports?: any, status?: string) => { socket.emit('switch_output', { stationId: station.id, portGroups: ports, status, }) socket.emit('switch_ports_status', { stationId: station.id, ports: Array.isArray(ports) && ports?.length > 0 ? ports?.flat() : [], }) }, }) // Connect và login await infoSwitch.connect() await infoSwitch.login() await infoSwitch.getPorts() this.switchControl.set(ip, infoSwitch) this.setTimeoutConnect(station.id, infoSwitch) } catch (error) { console.log(error) } } private async clearLineBeforeConnect(stationId: number, clearLine: number) { const station = await Station.find(stationId) if (!station) { console.log('[ERROR connect station] Not found!') return } console.log('Clearing line', clearLine, 'on station', station.name) await this.handleStationOperation(stationId, async (stationCon) => { stationCon.writeCommand(`\r\nclear line ${clearLine}\r\n`) await sleep(500) stationCon.writeCommand(`\r\n\r\n`) }) } async saveState() { const newMap = new Map() this.lineMap.forEach((line, id) => { if (line && line.config) { newMap.set(id, { config: { ...line.config, status: 'disconnected', userEmailOpenCLI: '', userOpenCLI: '', openCLI: false, listFeatureTested: [''], output: line.config.output.slice(-5000) || '', }, } 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) } private keepConnectAPC = (ip: string, io: any) => { if (this.intervalKeepConnect[`${ip}`]) { clearInterval(this.intervalKeepConnect[`${ip}`]) delete this.intervalKeepConnect[`${ip}`] } const interval = setInterval(() => { const apcConnect = this.apcsControl.get(ip) if (apcConnect && apcConnect.status === 'CONNECTED') { apcConnect._send('ENTER') } }, 40000) this.intervalKeepConnect[`${ip}`] = interval } private async checkBaudByClearLine( stationId: number, lineClear: number, lineId: number, io: any ) { const station = await Station.find(stationId) if (!station) { console.log('[ERROR connect station] Not found!') return } await this.handleStationOperation(stationId, async (stationCon) => { stationCon.writeCommand(`\r\n`) await sleep(500) stationCon.writeCommand(`show line\r\n`) await sleep(2000) }) const stationConn = this.stationMap.get(stationId) if (stationConn) { const buffer = stationConn?.config?.output || '' const result = this.detectBaudFromShowLine(buffer) const found = result.find((x) => x.clearLine === lineClear) if (found) { const line = this.lineMap.get(lineId) if (line) { line.config.baud = found.baud this.lineMap.set(lineId, line) io.emit('update_baud', { stationId, lineId, data: found.baud, }) } } } } private async setBaudByClearLine( stationId: number, lineClear: number, baud: number, lineId: number, io: any ) { const station = await Station.find(stationId) if (!station) { console.log('[ERROR connect station] Not found!') return } // Kết nối tới station qua Telnet / Socket const client = new net.Socket() let buffer = '' return new Promise((resolve, reject) => { client.setTimeout(8000) client.connect(station.port, station.ip, async () => { console.log(`Connected to station ${station.name} (${station.ip})`) // Gửi lệnh clear line client.write(`conf t\r\n`) await sleep(500) client.write(`line ${lineClear}\r\n`) await sleep(500) client.write(`speed ${baud.toString()}\r\n`) await sleep(500) client.write(`end\r\n`) await sleep(500) client.write(`\r\n`) await sleep(500) client.write(`show line\r\n`) await sleep(2000) client.destroy() resolve() }) client.on('data', (data) => { const text = data.toString() buffer += text }) client.on('error', (err) => { console.error(`Error clearing line ${lineClear}:`, err) resolve() }) client.on('close', () => { console.log(`Station connection closed (line ${lineClear})`) const result = this.detectBaudFromShowLine(buffer) const found = result.find((x) => x.clearLine === lineClear) if (found) { const line = this.lineMap.get(lineId) if (line) { line.config.baud = found.baud io.emit('update_baud', { stationId, lineId, data: found.baud, }) } } resolve() }) client.on('timeout', () => { console.log(`Station connection timeout (line ${lineClear})`) client.destroy() resolve() }) }) } private detectBaudFromShowLine(output: string) { const lines = output.split(/\r?\n/) const result: { clearLine: number; baud: number }[] = [] const regex = /^\s*\S+\s+(\d+)\s+\S+\s+(\d+)\/(\d+)/ for (const line of lines) { const match = line.replace('*', '').match(regex) if (match) { const clearLine = Number.parseInt(match[1], 10) const baud = Number.parseInt(match[2], 10) result.push({ clearLine, baud }) } } return result } private async getHistory(stationId: number, lineId: number): Promise { const key = `station:${stationId}:line:${lineId}:history` const items = await redis.zrange(key, 0, -1) return items.map((i) => JSON.parse(i)) } async waitUntilAllReady(lineIds: number[]) { await sleep(5000) return new Promise((resolve) => { const interval = setInterval(() => { let allReady = true const results = [] for (const lineId of lineIds) { const line = this.lineMap.get(lineId) if (!line || !line.dataDPELP) { allReady = false break } results.push(line.dataDPELP) } if (allReady) { clearInterval(interval) console.log('[DPELP] All lines ready') resolve(results) } }, 5000) // check mỗi 5 giây }) } generateTable(results: any) { let html = ` ` for (const item of results) { if (!item) continue const licenses = Array.isArray(item.license) ? [...new Set(item.license)] : item.license ? [item.license] : [] // format license thành 3 cột const licenseHTML = `
${licenses.map((l: string) => `
${l}
`).join('')}
` html += ` ` } html += `
Line PID SN MAC IOS License Summary Issues
${item.line || ''}
${item.pid || ''}
${item.vid || ''}
${item.sn || ''} ${item.mac || ''} ${item.ios || ''} ${licenseHTML} ${item.summary || ''} ${item.issues?.length ? `- ` + item.issues.join(`
- `) : '- No issues detected.'}
\n\n` return html } shortenResult(text: string) { const match = text.match(/RESULT:.*?(?=SUMMARY:)/s); return match ? match[0].trim().replace(/,\s*$/, '') : text; } /** * Generates a Zulip-compatible Markdown table string from the results array. * Uses
to force line breaks within the License cell. * @param {Array} results - The array of data objects. * @returns {string} The Markdown table string. */ generateZulipMessage(results: any[]) { let msg = `` msg += `| Line | PID | SN | MAC | IOS | License | Issues |\n` msg += `| ---- | ---- | ---- | ---- | ---- | ---- | ---- |\n` for (const item of results) { if (!item) continue // Format licenses const licenses = Array.isArray(item.license) ? [...new Set(item.license)] : item.license ? [item.license] : [] const licenseMd = licenses.length ? licenses.map((l) => `• ${l}`).join(' --') : '' // Format issues const issuesMd = item.issues?.length ? item.issues.map((i: string) => `• ${i.replace('|', '')}`).join(' --') : '- No issues detected.' const issue = item.issues?.length ? item.issues.join("\n") : "- No issues detected." const shortenedIssue = this.shortenResult(issue).split("\n") const issuesMdShort = shortenedIssue.map((i: string) => `• ${i.replace('|', '')}`).join(' --') msg += `| ${item.line || ''}` + ` | ${item.pid || ''} ${item.vid ? ` (${item.vid})` : ''}` + ` | ${item.sn || ''}` + ` | ${item.mac || ''}` + ` | ${item.ios || ''}` + ` | ${licenseMd}` + ` | ${issuesMdShort}` + ` |\n` } return msg } private async connectStation(station: Station) { try { const stationConn = new StationConnection({ id: station.id, port: station.port, ip: station.ip, name: station.name, output: '', status: '', }) this.stationMap.set(station.id, stationConn) await stationConn.connect() stationConn.writeCommand('\r\n') this.setTimeoutConnect(station.id, stationConn) this.keepConnectStation(station.id) } catch (error) { console.log(error) } } /** * Hàm xử lý chung cho mọi action (write command, runScript, v.v.) */ async handleStationOperation( stationId: number, action: StationAction, options: HandleOptions = {} ): Promise { try { const station = this.stationMap.get(stationId) // console.log(line?.config) if (station && station.config.status === 'connected') { this.setTimeoutConnect(stationId, station) // await sleep(500) await action(station, options) } else { const stationData = await Station.findBy('id', stationId) if (stationData) { await this.connectStation(stationData) const stationReconnect = this.stationMap.get(stationId) if (stationReconnect) { this.setTimeoutConnect(stationId, stationReconnect) await sleep(100) await action(stationReconnect, options) } } else { console.log('Station not found') } } } catch (err: any) { console.log('Station connect error:', err.message) } } private keepConnectStation = (id: number) => { if (this.intervalKeepConnect[`${id}`]) { clearInterval(this.intervalKeepConnect[`${id}`]) delete this.intervalKeepConnect[`${id}`] } const interval = setInterval(async () => { const station = this.stationMap.get(id) if (station) { await this.handleStationOperation(id, async (stationCon) => { stationCon.writeCommand(`\r\n`) }) } }, 120000) this.intervalKeepConnect[`${id}`] = interval } }