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((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: '', }) } }