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' 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 = new Map() lineMap: Map = new Map() // key = lineId lineConnecting: number[] = [] // key = lineId userConnecting: Map = new Map() 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' 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(() => { io.to(socket.id).emit( 'init', Array.from(this.lineMap.values()).map((el) => el.config) ) }, 500) socket.on('disconnect', () => { console.log(`FE disconnected: ${socket.id}`) this.userConnecting.delete(userId) 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 await this.connectLine(io, linesData, stationData) }) socket.on('write_command_line_from_web', async (data) => { const { lineIds, stationId, command } = data for (const lineId of lineIds) { const line = this.lineMap.get(lineId) if (line && line.config.status === 'connected') { this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId) this.setTimeoutConnect(lineId, line) line.writeCommand(command) } else { if (this.lineConnecting.includes(lineId)) continue 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) { this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId) this.setTimeoutConnect(lineId, lineReconnect) lineReconnect.writeCommand(command) } } else { io.emit('line_disconnected', { stationId, lineId, status: 'disconnected', }) io.emit('line_error', { lineId, error: 'Line not connected\r\n' }) } } } }) socket.on('run_scenario', async (data) => { const lineId = data.id const scenario = data.scenario const line = this.lineMap.get(lineId) if (line && line.config.status === 'connected') { this.setTimeoutConnect( lineId, line, scenario?.timeout ? Number(scenario?.timeout) + 180000 : 300000 ) line.runScript(scenario) } else { const linesData = await Line.findBy('id', lineId) const stationData = await Station.findBy('id', data.station_id) if (linesData && stationData) { await this.connectLine(io, [linesData], stationData) const lineReconnect = this.lineMap.get(lineId) if (lineReconnect) { this.setTimeoutConnect(lineId, lineReconnect, 300000) lineReconnect.runScript(scenario) } } else { io.emit('line_disconnected', { stationId: data.stationId, lineId, status: 'disconnected', }) io.emit('line_error', { lineId, error: 'Line not connected\r\n' }) } } }) socket.on('open_cli', async (data) => { const { lineId, userEmail, userName: name, stationId } = data const line = this.lineMap.get(lineId) if (line) { line.userOpenCLI({ userEmail, userName: name }) } 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) { line.userCloseCLI() } 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 () => { let getListSystemLogs = fs .readdirSync('storage/system_logs') .map((f) => 'storage/system_logs/' + f) io.to(socket.id).emit('list_logs', getListSystemLogs) }) 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) } }) }) socketServer.listen(SOCKET_IO_PORT, () => { console.log(`Socket server is running on port ${SOCKET_IO_PORT}`) }) return io } private async connectLine(socket: any, lines: Line[], station: Station) { try { this.stationMap.set(station.id, station) for (const line of lines) { const lineConn = new LineConnection( { id: line.id, port: line.port, ip: station.ip, lineNumber: line.lineNumber, stationId: station.id, apcName: line.apcName, output: '', status: '', openCLI: false, userEmailOpenCLI: '', userOpenCLI: '', data: [], }, socket ) await lineConn.connect() lineConn.writeCommand('\r\n\r\n') this.lineMap.set(line.id, lineConn) this.setTimeoutConnect(line.id, lineConn) } } catch (error) { console.log(error) } } private setTimeoutConnect = (lineId: number, lineConn: LineConnection, timeout = 180000) => { if (this.intervalMap[`${lineId}`]) { clearInterval(this.intervalMap[`${lineId}`]) delete this.intervalMap[`${lineId}`] } const interval = setInterval(() => { lineConn.disconnect() // this.lineMap.delete(lineId) if (this.intervalMap[`${lineId}`]) { clearInterval(this.intervalMap[`${lineId}`]) delete this.intervalMap[`${lineId}`] } }, timeout) this.intervalMap[`${lineId}`] = interval } }