587 lines
17 KiB
TypeScript
587 lines
17 KiB
TypeScript
import { textfsmResults } from './../ultils/templates/index.js'
|
|
import net from 'node:net'
|
|
import {
|
|
appendLog,
|
|
cleanData,
|
|
getLogWithTimeScenario,
|
|
isValidJson,
|
|
sleep,
|
|
} from '../ultils/helper.js'
|
|
import Scenario from '#models/scenario'
|
|
import Station from '#models/station'
|
|
import APCController from './apc_connection.js'
|
|
|
|
interface LineConfig {
|
|
id: number
|
|
port: number
|
|
lineNumber: number
|
|
ip: string
|
|
stationId: number
|
|
stationName: string
|
|
stationIp: string
|
|
apcName?: string
|
|
outlet: number
|
|
output: string
|
|
status: string
|
|
baud: number
|
|
openCLI: boolean
|
|
userEmailOpenCLI: string
|
|
userOpenCLI: string
|
|
inventory: string
|
|
latestScenario?: {
|
|
name: string
|
|
time: number
|
|
}
|
|
data: {
|
|
command: string
|
|
output: string
|
|
textfsm: string
|
|
}[]
|
|
commands: string[]
|
|
// history: string
|
|
}
|
|
|
|
/** HISTORY
|
|
* PID
|
|
* SN
|
|
* VID
|
|
* Timestamp
|
|
* Scenario
|
|
*/
|
|
|
|
interface User {
|
|
userEmail: string
|
|
userName: string
|
|
}
|
|
|
|
export default class LineConnection {
|
|
public client: net.Socket
|
|
public config: LineConfig
|
|
public readonly socketIO: any
|
|
private outputBuffer: string
|
|
private isRunningScript: boolean
|
|
private connecting: boolean
|
|
private waitingScenario: boolean
|
|
private outputInventory: string
|
|
private outputScenario: string
|
|
private bufferCommand: string
|
|
private retryConnect: number
|
|
|
|
constructor(config: LineConfig, socketIO: any) {
|
|
this.config = config
|
|
this.socketIO = socketIO
|
|
this.client = new net.Socket()
|
|
this.outputBuffer = ''
|
|
this.isRunningScript = false
|
|
this.connecting = false
|
|
this.waitingScenario = false
|
|
this.outputInventory = ''
|
|
this.outputScenario = ''
|
|
this.bufferCommand = ''
|
|
this.retryConnect = 0
|
|
}
|
|
|
|
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)
|
|
console.log(`🔌 Connecting to line ${lineNumber} (${ip}:${port})...`)
|
|
this.client.connect(port, ip, () => {
|
|
if (resolvedOrRejected) return
|
|
resolvedOrRejected = true
|
|
|
|
console.log(`[${Date.now()}] ✅ 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) => {
|
|
let message = this.connecting ? cleanData(data.toString()) : data.toString()
|
|
let rawData = ''
|
|
if (this.isRunningScript) {
|
|
this.waitingScenario = true
|
|
this.outputBuffer += message
|
|
this.outputScenario += message
|
|
if (!this.config.inventory)
|
|
this.outputInventory = this.outputInventory.slice(-3000) + message
|
|
}
|
|
if (message.includes('--More--')) this.writeCommand(' ')
|
|
|
|
// 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 {
|
|
rawData += char
|
|
}
|
|
}
|
|
this.config.output += cleanData(rawData)
|
|
this.config.output = this.config.output.slice(-15000)
|
|
this.socketIO.emit('line_output', {
|
|
stationId,
|
|
lineId: id,
|
|
data: message,
|
|
commands: this.config.commands,
|
|
})
|
|
if (!this.config.inventory) {
|
|
setTimeout(() => {
|
|
this.getInventory()
|
|
}, 3000)
|
|
}
|
|
appendLog(
|
|
cleanData(message),
|
|
this.config.stationId,
|
|
this.config.stationName,
|
|
this.config.stationIp,
|
|
this.config.lineNumber
|
|
)
|
|
})
|
|
|
|
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: '\r\n' + err.message + '\r\n',
|
|
})
|
|
resolve()
|
|
})
|
|
|
|
this.client.on('close', () => {
|
|
console.log(`[${Date.now()}] 🔌 Line ${lineNumber} disconnected`)
|
|
this.config.status = 'disconnected'
|
|
// this.config.inventory = undefined
|
|
this.socketIO.emit('line_disconnected', {
|
|
stationId,
|
|
lineId: id,
|
|
lineNumber,
|
|
status: 'disconnected',
|
|
})
|
|
})
|
|
|
|
this.client.on('timeout', () => {
|
|
if (resolvedOrRejected) return
|
|
resolvedOrRejected = true
|
|
const message = 'Connection timeout!!\r\n'
|
|
this.config.output += message
|
|
this.socketIO.emit('line_output', {
|
|
stationId,
|
|
lineId: id,
|
|
data: message,
|
|
})
|
|
appendLog(
|
|
cleanData(message),
|
|
this.config.stationId,
|
|
this.config.stationName,
|
|
this.config.stationIp,
|
|
this.config.lineNumber
|
|
)
|
|
console.log(`⏳ Connection timeout line ${lineNumber}`)
|
|
this.client.destroy()
|
|
resolve()
|
|
// reject(new Error('Connection timeout'))
|
|
})
|
|
})
|
|
}
|
|
|
|
async writeCommand(cmd: string | Buffer<ArrayBuffer>, userName = '') {
|
|
if (this.client.destroyed) {
|
|
console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`)
|
|
// if (this.retryConnect <= 3) {
|
|
// await sleep(2000)
|
|
// console.log('Retry connect times', this.retryConnect)
|
|
// this.retryConnect += 1
|
|
// await this.connect()
|
|
// await this.writeCommand(cmd)
|
|
// }
|
|
return
|
|
}
|
|
|
|
this.client.write(cmd)
|
|
if (userName) {
|
|
// appendLog(
|
|
// `\n---${userName}---\n`,
|
|
// this.config.stationId,
|
|
// this.config.lineNumber,
|
|
// this.config.port
|
|
// )
|
|
// for (const char of command) {
|
|
// if (char === '\x7F') this.bufferCommand = this.bufferCommand.slice(0, -1)
|
|
// else if (char === '\r' && cleanData(this.bufferCommand).length > 0) {
|
|
// this.config.commands = [
|
|
// cleanData(this.bufferCommand.replace('\r', '')),
|
|
// ...this.config.commands.filter(
|
|
// (el) => el !== cleanData(this.bufferCommand.replace('\r', ''))
|
|
// ),
|
|
// ].slice(0, 10)
|
|
// this.bufferCommand = ''
|
|
// } else this.bufferCommand += char
|
|
// }
|
|
}
|
|
}
|
|
|
|
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, userName: string) {
|
|
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()
|
|
this.outputScenario += `\n\n---start-scenarios---${now}---${userName}---\n---scenario---${script?.title}---${now}---\n`
|
|
appendLog(
|
|
`\n\n---start-scenarios---${now}---${userName}---\n---scenario---${script?.title}---${now}---\n`,
|
|
this.config.stationId,
|
|
this.config.stationName,
|
|
this.config.stationIp,
|
|
this.config.lineNumber
|
|
)
|
|
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.outputScenario = ''
|
|
this.config.output += 'Timeout run scenario'
|
|
this.socketIO.emit('line_output', {
|
|
stationId: this.config.stationId,
|
|
lineId: this.config.id,
|
|
data: 'Timeout run scenario',
|
|
})
|
|
this.outputScenario += `\n---end-scenarios---${now}---${userName}---\n`
|
|
appendLog(
|
|
`\n---end-scenarios---${now}---${userName}---\n`,
|
|
this.config.stationId,
|
|
this.config.stationName,
|
|
this.config.stationIp,
|
|
this.config.lineNumber
|
|
)
|
|
// reject(new Error('Script timeout'))
|
|
}, script.timeout || 300000)
|
|
|
|
const runStep = async (index: number) => {
|
|
if (index >= steps.length) {
|
|
if (this.waitingScenario) {
|
|
this.waitingScenario = false
|
|
setTimeout(() => {
|
|
runStep(index)
|
|
}, 5000)
|
|
return
|
|
} else clearTimeout(timeoutTimer)
|
|
this.isRunningScript = false
|
|
this.outputBuffer = ''
|
|
this.outputScenario += `\n---end-scenarios---${now}---${userName}---\n`
|
|
appendLog(
|
|
`\n---end-scenarios---${now}---${userName}---\n`,
|
|
this.config.stationId,
|
|
this.config.stationName,
|
|
this.config.stationIp,
|
|
this.config.lineNumber
|
|
)
|
|
|
|
const logScenarios = getLogWithTimeScenario(this.outputScenario, now) || ''
|
|
const data = textfsmResults(logScenarios, '')
|
|
try {
|
|
data.forEach((item) => {
|
|
if (item?.textfsm && isValidJson(item?.textfsm)) {
|
|
if (
|
|
['show inventory', 'sh inventory', 'show inv', 'sh inv'].includes(item.command)
|
|
) {
|
|
this.config.inventory = JSON.parse(item.textfsm)[0]
|
|
}
|
|
item.textfsm = JSON.parse(item.textfsm)
|
|
}
|
|
})
|
|
this.config.data = data
|
|
this.socketIO.emit('data_textfsm', {
|
|
stationId: this.config.stationId,
|
|
lineId: this.config.id,
|
|
data,
|
|
inventory: this.config.inventory || null,
|
|
latestScenario: this.config.latestScenario || null,
|
|
})
|
|
} catch (error) {
|
|
console.log(error)
|
|
}
|
|
|
|
this.outputScenario = ''
|
|
resolve(true)
|
|
return
|
|
}
|
|
|
|
const step = steps[index]
|
|
this.outputScenario += `\n---send-command---"${step?.send ?? ''}"---${now}---\n`
|
|
appendLog(
|
|
`\n---send-command---"${step?.send ?? ''}"---${now}---\n`,
|
|
this.config.stationId,
|
|
this.config.stationName,
|
|
this.config.stationIp,
|
|
this.config.lineNumber
|
|
)
|
|
let repeatCount = Number(step.repeat) || 1
|
|
const sendCommand = async () => {
|
|
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)
|
|
// }
|
|
// }
|
|
|
|
const matched = await this.waitForExpect(step.expect, Number(step?.timeout) || 60000)
|
|
if (matched) 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,
|
|
})
|
|
appendLog(
|
|
`\n-------${user.userName}-------\n`,
|
|
this.config.stationId,
|
|
this.config.stationName,
|
|
this.config.stationIp,
|
|
this.config.lineNumber
|
|
)
|
|
}
|
|
|
|
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: '',
|
|
})
|
|
}
|
|
|
|
waitForExpect = async (expect: string, timeout = 60000) => {
|
|
const start = Date.now()
|
|
while (Date.now() - start < timeout) {
|
|
if (this.outputBuffer.includes(expect)) {
|
|
this.outputBuffer = ''
|
|
return true
|
|
}
|
|
await sleep(200)
|
|
}
|
|
return false
|
|
}
|
|
|
|
async apcControl(action: 'on' | 'off' | 'restart') {
|
|
try {
|
|
const station = await Station.find(this.config.stationId)
|
|
if (!station) throw new Error('Station not found')
|
|
|
|
const apcName = this.config.apcName || 'apc_1'
|
|
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,
|
|
number: this.config.lineNumber,
|
|
onData: (data: string, status: string) => {
|
|
this.config.output += data
|
|
this.socketIO.emit('apc_output', {
|
|
stationId: this.config.stationId,
|
|
lineId: this.config.id,
|
|
apcNumber: apcName === 'apc_1' ? 1 : 2,
|
|
data,
|
|
status,
|
|
})
|
|
appendLog(
|
|
cleanData(data),
|
|
this.config.stationId,
|
|
this.config.stationName,
|
|
this.config.stationIp,
|
|
this.config.lineNumber
|
|
)
|
|
},
|
|
})
|
|
|
|
// Connect và login
|
|
await apc.connect()
|
|
await apc.login()
|
|
|
|
// Thực thi hành động
|
|
this.socketIO.emit('apc_status', {
|
|
stationId: this.config.stationId,
|
|
lineId: this.config.id,
|
|
action,
|
|
status: 'running',
|
|
})
|
|
|
|
switch (action) {
|
|
case 'on':
|
|
await apc.turnOnOutlet(this.config.outlet)
|
|
break
|
|
case 'off':
|
|
await apc.turnOffOutlet(this.config.outlet)
|
|
break
|
|
case 'restart':
|
|
await apc.restartOutlet(this.config.outlet)
|
|
break
|
|
}
|
|
|
|
// Hoàn thành
|
|
this.socketIO.emit('apc_status', {
|
|
stationId: this.config.stationId,
|
|
lineId: this.config.id,
|
|
action,
|
|
status: 'done',
|
|
})
|
|
|
|
apc.disconnect()
|
|
} catch (error) {
|
|
const msg = (error as Error).message
|
|
console.error('APC Control error:', msg)
|
|
this.socketIO.emit('apc_status', {
|
|
stationId: this.config.stationId,
|
|
lineId: this.config.id,
|
|
action,
|
|
status: 'error',
|
|
message: msg,
|
|
})
|
|
}
|
|
}
|
|
|
|
getInventory = () => {
|
|
const data = textfsmResults(this.outputInventory, 'show inventory')
|
|
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)
|
|
}
|
|
})
|
|
if (this.config.inventory) {
|
|
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,
|
|
})
|
|
this.outputInventory = ''
|
|
}
|
|
} catch (error) {
|
|
console.log(error)
|
|
}
|
|
}
|
|
|
|
// Gửi nhiều ký tự ESC để vào ROMMON
|
|
breakSpam() {
|
|
console.log('SPAM Break to line:', this.config.lineNumber)
|
|
let count = 0
|
|
const escInterval = setInterval(() => {
|
|
if (count >= 10) {
|
|
clearInterval(escInterval)
|
|
return
|
|
}
|
|
this.client.write(Buffer.from([0xff, 0xf3])) // Ctrl + Break
|
|
count++
|
|
}, 1)
|
|
}
|
|
|
|
async setBaud(baud: number) {
|
|
this.writeCommand('enable\r\n')
|
|
await sleep(500)
|
|
this.writeCommand('configure terminal\r\n')
|
|
await sleep(500)
|
|
this.writeCommand('line console 0\r\n')
|
|
await sleep(500)
|
|
this.writeCommand(`speed ${baud.toString()}\r\n`)
|
|
await sleep(500)
|
|
this.writeCommand('end\r\n')
|
|
await sleep(500)
|
|
this.writeCommand('write memory\r\n')
|
|
this.writeCommand('\r\n')
|
|
}
|
|
}
|