453 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
			
		
		
	
	
			453 lines
		
	
	
		
			13 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'
 | 
						|
import Station from '#models/station'
 | 
						|
import APCController from './apc_connection.js'
 | 
						|
 | 
						|
interface LineConfig {
 | 
						|
  id: number
 | 
						|
  port: number
 | 
						|
  lineNumber: number
 | 
						|
  ip: string
 | 
						|
  stationId: number
 | 
						|
  apcName?: string
 | 
						|
  outlet: number
 | 
						|
  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)
 | 
						|
      console.log(`🔌 Connecting to line ${lineNumber} (${ip}:${port})...`)
 | 
						|
      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 (message.includes('--More--')) this.writeCommand('   ')
 | 
						|
        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
 | 
						|
        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.lineNumber,
 | 
						|
          this.config.port
 | 
						|
        )
 | 
						|
        console.log(`⏳ Connection timeout line ${lineNumber}`)
 | 
						|
        this.client.destroy()
 | 
						|
        resolve()
 | 
						|
        // 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)
 | 
						|
        //   }
 | 
						|
        // }
 | 
						|
 | 
						|
        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,
 | 
						|
    })
 | 
						|
  }
 | 
						|
 | 
						|
  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) => {
 | 
						|
          this.config.output += data
 | 
						|
          this.socketIO.emit('line_output', {
 | 
						|
            stationId: this.config.stationId,
 | 
						|
            lineId: this.config.id,
 | 
						|
            data: data,
 | 
						|
          })
 | 
						|
          appendLog(
 | 
						|
            cleanData(data),
 | 
						|
            this.config.stationId,
 | 
						|
            this.config.lineNumber,
 | 
						|
            this.config.port
 | 
						|
          )
 | 
						|
        },
 | 
						|
      })
 | 
						|
 | 
						|
      // 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,
 | 
						|
      })
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 |