1556 lines
49 KiB
TypeScript
1556 lines
49 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,
|
|
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 | 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(() => {
|
|
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
|
|
// 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, 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)
|
|
// 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 = 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 } = data
|
|
// Check station is active
|
|
const activeStation = await checkStationActive(stationId)
|
|
if (!activeStation) return
|
|
|
|
await this.handleLineOperation(
|
|
io,
|
|
stationId,
|
|
[lineId],
|
|
async (lineCon) => lineCon.runPhysicalTest(),
|
|
{}
|
|
)
|
|
})
|
|
|
|
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.sendReportSummary()
|
|
},
|
|
{}
|
|
)
|
|
})
|
|
})
|
|
|
|
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<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,
|
|
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<number, LineConnection>()
|
|
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<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
|
|
}
|
|
}
|