ATC_SIMPLE/BACKEND/providers/socket_io_provider.ts

1340 lines
42 KiB
TypeScript

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,
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 | unknown> | void
type StationAction = (
line: StationConnection,
options?: HandleOptions
) => Promise<void | unknown> | 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<number, StationConnection> = new Map() // key = stationId
lineMap: Map<number, LineConnection> = new Map() // key = lineId
userConnecting: Map<number, { userId: number; userName: string }> = new Map()
apcsControl: Map<string, APCController> = new Map()
switchControl: Map<string, SwitchController> = 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(() => {
io.to(socket.id).emit(
'init',
Array.from(this.lineMap.values()).map((el) => {
const config = el?.config || {}
if (config.status !== 'connected') {
config.runningScenario = ''
config.runningPhysical = false
}
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
await this.connectLine(io, linesData, stationData)
})
socket.on('write_command_line_from_web', async (data) => {
const { lineIds, stationId, command } = data
await this.handleLineOperation(
io,
stationId,
lineIds,
async (line) =>
command === 'spam_break' ? line.breakSpam() : line.writeCommand(command, userName),
{ command }
)
})
socket.on('run_scenario', async (data) => {
const lineId = data.id
const scenario = data.scenario
await this.handleLineOperation(
io,
data.stationId,
[lineId],
async (line) => line.runScript(scenario, userName),
{
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)
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', userName),
{ 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) => {
let getListSystemLogs = fs
.readdirSync('storage/system_logs')
.map((f) => 'storage/system_logs/' + f)
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 = 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
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
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
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
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_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 {
const lineIds = data.lineIds
const stationName = data.stationName || ''
const stationId = data.stationId || ''
const scenarioName = data.scenarioName || ''
const station = await Station.find(stationId)
// Check station sendWiki flag
console.log('[DPELP] Received run all dpelp', lineIds)
if (!station || !station?.send_wiki) 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 linkWiki =
process.env.LINK_WIKI || 'https://logs.danielvu.com/api/wiki/page/insert?title=Dev_test'
await axios.post(linkWiki, {
data: tableHTML,
titleAuto: `[${scenarioName || 'DPELP'}] - ${stationName} - ` + dataFormat,
})
await sendMessageToMail(
`[${scenarioName || 'DPELP'}] - ${stationName} - ${dataFormat}`,
tableHTML
)
await sendMessageToZulip(
'stream',
'ATC_Report',
station.name,
`\n\n---\n**[${scenarioName || 'DPELP'}] - ${stationName} - ${dataFormat}**\n\n` +
zulipMess
)
} catch (error) {
console.log(error)
}
})
socket.on('clear_terminal', async (data) => {
const { stationId, lineId } = data
await this.handleLineOperation(
io,
stationId,
[lineId],
async (lineCon) => lineCon.clearCLI(),
{}
)
})
socket.on('run_physical_test', async (data) => {
const { stationId, lineId } = data
await this.handleLineOperation(
io,
stationId,
[lineId],
async (lineCon) => lineCon.runPhysicalTest(),
{}
)
})
socket.on('end_run_physical_test', async (data) => {
const { stationId, lineId } = data
await this.handleLineOperation(
io,
stationId,
[lineId],
async (lineCon) => {
await lineCon.sendReportPhysicalTest()
lineCon.endTesting()
},
{}
)
})
socket.on('reset_physical_test', async (data) => {
const { stationId, lineId } = data
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
await this.handleLineOperation(
io,
stationId,
[lineId],
async (lineCon) => {
if (isReboot) {
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
await this.handleLineOperation(
io,
stationId,
[lineId],
async (lineCon) => {
lineCon.loadIosSwitch(iosName, userName)
},
{}
)
})
})
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
) {
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: [],
ports: [],
inventory: inventory,
runningPhysical: false,
runningScenario: '',
latestScenario: latestScenario,
},
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<void> {
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
)
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<number, LineConnection>()
this.lineMap.forEach((line, id) => {
if (line && line.config) {
newMap.set(id, {
config: {
...line.config,
status: 'disconnected',
userEmailOpenCLI: '',
userOpenCLI: '',
openCLI: false,
},
} 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<void>((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<HistoryItem[]> {
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<any[]>((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 = `<table border="1" cellpadding="6" style="border-collapse: collapse; font-size: 14px;">
<tr>
<th style="text-align:center; width:40px;">Line</th>
<th style="width:190px;">PID</th>
<th style="text-align:center; width:140px;">SN</th>
<th>MAC</th>
<th style="width:270px; text-align:center;">IOS</th>
<th style="width:200px; word-wrap: break-word; white-space: normal;">License</th>
<th>Summary</th>
<th>Issues</th>
</tr>
`
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 = `
<div style="
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px;
width: 100%;
word-break: break-word;
">
${licenses.map((l: string) => `<div>${l}</div>`).join('')}
</div>
`
html += `
<tr>
<td style="text-align:center;">${item.line || ''}</td>
<td><div style="display:flex;"><div style="width:150px; margin-right:4px;">${item.pid || ''}</div><div> ${item.vid || ''}</div></div></td>
<td style="text-align:center;">${item.sn || ''}</td>
<td>${item.mac || ''}</td>
<td style="width:270px">${item.ios || ''}</td>
<td style="width:200px;">${licenseHTML}</td>
<td style="width:200px; text-wrap: wrap;">${item.summary || ''}</td>
<td>${item.issues?.length ? `- ` + item.issues.join(`<br>- `) : '- No issues detected.'}</td>
</tr>
`
}
html += `</table>\n\n`
return html
}
/**
* Generates a Zulip-compatible Markdown table string from the results array.
* Uses <br> to force line breaks within the License cell.
* @param {Array<Object>} results - The array of data objects.
* @returns {string} The Markdown table string.
*/
generateZulipMessage(results: any[]) {
let msg = ``
msg += `| Line | PID | SN | MAC | IOS | License | Summary | 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}`).join(' --')
: '- No issues detected.'
msg +=
`| ${item.line || ''}` +
` | ${item.pid || ''} ${item.vid ? ` (${item.vid})` : ''}` +
` | ${item.sn || ''}` +
` | ${item.mac || ''}` +
` | ${item.ios || ''}` +
` | ${licenseMd}` +
` | ${item.summary || ''}` +
` | ${issuesMd}` +
` |\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<void> {
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
}
}