ATC_SIMPLE/BACKEND/app/services/line_connection.ts

336 lines
9.3 KiB
TypeScript

import { textfsmResults } from './../ultils/templates/index.js'
import fs from 'node:fs'
import net from 'node:net'
import {
appendLog,
cleanData,
getLogWithTimeScenario,
getPathLog,
isValidJson,
sleep,
} from '../ultils/helper.js'
import Scenario from '#models/scenario'
interface LineConfig {
id: number
port: number
lineNumber: number
ip: string
stationId: number
apcName?: string
output: string
status: string
openCLI: boolean
userEmailOpenCLI: string
userOpenCLI: string
inventory?: string
latestScenario?: {
name: string
time: number
}
data: {
command: string
output: string
textfsm: string
}[]
}
interface User {
userEmail: string
userName: string
}
export default class LineConnection {
public client: net.Socket
public readonly config: LineConfig
public readonly socketIO: any
private outputBuffer: string
private isRunningScript: boolean
private connecting: boolean
constructor(config: LineConfig, socketIO: any) {
this.config = config
this.socketIO = socketIO
this.client = new net.Socket()
this.outputBuffer = ''
this.isRunningScript = false
this.connecting = false
}
connect(timeoutMs = 5000) {
return new Promise<void>((resolve, reject) => {
const { ip, port, lineNumber, id, stationId } = this.config
let resolvedOrRejected = false
// Set timeout
this.client.setTimeout(timeoutMs)
this.client.connect(port, ip, () => {
if (resolvedOrRejected) return
resolvedOrRejected = true
console.log(`✅ Connected to line ${lineNumber} (${ip}:${port})`)
this.connecting = true
setTimeout(() => {
this.config.status = 'connected'
this.connecting = false
this.socketIO.emit('line_connected', {
stationId,
lineId: id,
lineNumber,
status: 'connected',
})
resolve()
}, 1000)
})
this.client.on('data', (data) => {
if (this.connecting) return
let message = data.toString()
if (this.isRunningScript) this.outputBuffer += message
// let output = cleanData(message)
// console.log(`📨 [${this.config.port}] ${message}`)
// Handle netOutput with backspace support
for (const char of message) {
if (char === '\x7F' || char === '\x08') {
this.config.output = this.config.output.slice(0, -1)
// message = message.slice(0, -1)
} else {
this.config.output += cleanData(char)
}
}
this.config.output = this.config.output.slice(-15000)
this.socketIO.emit('line_output', {
stationId,
lineId: id,
data: message,
})
appendLog(
cleanData(message),
this.config.stationId,
this.config.lineNumber,
this.config.port
)
})
this.client.on('error', (err) => {
if (resolvedOrRejected) return
resolvedOrRejected = true
console.error(`❌ Error line ${lineNumber}:`, err.message)
this.config.output += err.message
this.socketIO.emit('line_error', {
stationId,
lineId: id,
error: err.message + '\r\n',
})
reject(err)
})
this.client.on('close', () => {
console.log(`🔌 Line ${lineNumber} disconnected`)
this.config.status = 'disconnected'
this.socketIO.emit('line_disconnected', {
stationId,
lineId: id,
lineNumber,
status: 'disconnected',
})
})
this.client.on('timeout', () => {
if (resolvedOrRejected) return
resolvedOrRejected = true
console.log(`⏳ Connection timeout line ${lineNumber}`)
this.client.destroy()
// reject(new Error('Connection timeout'))
})
})
}
writeCommand(cmd: string) {
if (this.client.destroyed) {
console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`)
return
}
this.client.write(`${cmd}`)
}
disconnect() {
try {
this.client.destroy()
this.config.status = 'disconnected'
this.socketIO.emit('line_disconnected', {
...this.config,
status: 'disconnected',
})
console.log(`🔻 Closed connection to line ${this.config.lineNumber}`)
} catch (e) {
console.error('Error closing line:', e)
}
}
async runScript(script: Scenario) {
if (!this.client || this.client.destroyed) {
console.log('Not connected')
this.isRunningScript = false
this.outputBuffer = ''
return
}
if (this.isRunningScript) {
console.log('Script already running')
return
}
this.isRunningScript = true
const now = Date.now()
appendLog(
`\n\n---start-scenarios---${now}---\n---scenario---${script?.title}---${now}---\n`,
this.config.stationId,
this.config.lineNumber,
this.config.port
)
this.config.latestScenario = {
name: script?.title,
time: now,
}
const steps = typeof script?.body === 'string' ? JSON.parse(script?.body) : []
let stepIndex = 0
return new Promise((resolve, reject) => {
const timeoutTimer = setTimeout(() => {
this.isRunningScript = false
this.outputBuffer = ''
this.config.output += 'Timeout run scenario'
this.socketIO.emit('line_output', {
stationId: this.config.stationId,
lineId: this.config.id,
data: 'Timeout run scenario',
})
appendLog(
`\n---end-scenarios---${now}---\n`,
this.config.stationId,
this.config.lineNumber,
this.config.port
)
// reject(new Error('Script timeout'))
}, script.timeout || 300000)
const runStep = async (index: number) => {
if (index >= steps.length) {
clearTimeout(timeoutTimer)
this.isRunningScript = false
this.outputBuffer = ''
appendLog(
`\n---end-scenarios---${now}---\n`,
this.config.stationId,
this.config.lineNumber,
this.config.port
)
const pathLog = getPathLog(
this.config.stationId,
this.config.lineNumber,
this.config.port
)
if (pathLog)
fs.readFile(pathLog, 'utf8', async (err, content) => {
if (err) return
const logScenarios = getLogWithTimeScenario(content, now) || ''
const data = textfsmResults(logScenarios, '')
try {
data.forEach((item) => {
if (item?.textfsm && isValidJson(item?.textfsm)) {
if (
['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(
item.command
)
) {
this.config.inventory = JSON.parse(item.textfsm)[0]
}
item.textfsm = JSON.parse(item.textfsm)
}
})
this.config.data = data
this.socketIO.emit('data_textfsm', {
stationId: this.config.stationId,
lineId: this.config.id,
data,
inventory: this.config.inventory || null,
latestScenario: this.config.latestScenario || null,
})
} catch (error) {
console.log(error)
}
})
resolve(true)
return
}
const step = steps[index]
appendLog(
`\n---send-command---"${step?.send ?? ''}"---${now}---\n`,
this.config.stationId,
this.config.lineNumber,
this.config.port
)
let repeatCount = Number(step.repeat) || 1
const sendCommand = () => {
if (repeatCount <= 0) {
// Done → next step
stepIndex++
return runStep(stepIndex)
}
if (step.send) {
this.writeCommand(step?.send + '\r\n')
}
repeatCount--
setTimeout(() => sendCommand(), Number(step?.delay) || 500)
}
// Nếu expect rỗng → gửi ngay
if (!step?.expect || step?.expect.trim() === '') {
setTimeout(() => sendCommand(), Number(step?.delay) || 500)
return
}
while (this.outputBuffer) {
await sleep(200)
if (this.outputBuffer.includes(step.expect)) {
this.outputBuffer = ''
setTimeout(() => sendCommand(), Number(step?.delay) || 500)
}
}
}
runStep(stepIndex)
})
}
userOpenCLI(user: User) {
this.config.openCLI = true
this.config.userEmailOpenCLI = user.userEmail
this.config.userOpenCLI = user.userName
this.socketIO.emit('user_open_cli', {
stationId: this.config.stationId,
lineId: this.config.id,
userEmailOpenCLI: user.userEmail,
userOpenCLI: user.userName,
})
}
userCloseCLI() {
this.config.openCLI = false
this.config.userEmailOpenCLI = ''
this.config.userOpenCLI = ''
this.socketIO.emit('user_close_cli', {
stationId: this.config.stationId,
lineId: this.config.id,
userEmailOpenCLI: '',
})
}
}