Refactor log handling and improve output buffering

Updated log file naming to include station name and IP, and refactored appendLog to use new format. Enhanced frontend output buffering for lines to reduce UI update frequency. Improved connection retry logic for switch connections and adjusted socket.io provider timeouts. Updated DrawerLogs format description and removed unnecessary state changes in TerminalXTerm.
This commit is contained in:
nguyentrungthat 2025-11-25 11:32:07 +07:00
parent dc47636c96
commit 36d7438055
7 changed files with 140 additions and 40 deletions

View File

@ -17,6 +17,8 @@ interface LineConfig {
lineNumber: number lineNumber: number
ip: string ip: string
stationId: number stationId: number
stationName: string
stationIp: string
apcName?: string apcName?: string
outlet: number outlet: number
output: string output: string
@ -36,8 +38,17 @@ interface LineConfig {
textfsm: string textfsm: string
}[] }[]
commands: string[] commands: string[]
// history: string
} }
/** HISTORY
* PID
* SN
* VID
* Timestamp
* Scenario
*/
interface User { interface User {
userEmail: string userEmail: string
userName: string userName: string
@ -81,7 +92,7 @@ export default class LineConnection {
if (resolvedOrRejected) return if (resolvedOrRejected) return
resolvedOrRejected = true resolvedOrRejected = true
console.log(`✅ Connected to line ${lineNumber} (${ip}:${port})`) console.log(`[${Date.now()}] ✅ Connected to line ${lineNumber} (${ip}:${port})`)
this.connecting = true this.connecting = true
setTimeout(() => { setTimeout(() => {
this.config.status = 'connected' this.config.status = 'connected'
@ -135,8 +146,9 @@ export default class LineConnection {
appendLog( appendLog(
cleanData(message), cleanData(message),
this.config.stationId, this.config.stationId,
this.config.lineNumber, this.config.stationName,
this.config.port this.config.stationIp,
this.config.lineNumber
) )
}) })
@ -154,7 +166,7 @@ export default class LineConnection {
}) })
this.client.on('close', () => { this.client.on('close', () => {
console.log(`🔌 Line ${lineNumber} disconnected`) console.log(`[${Date.now()}] 🔌 Line ${lineNumber} disconnected`)
this.config.status = 'disconnected' this.config.status = 'disconnected'
// this.config.inventory = undefined // this.config.inventory = undefined
this.socketIO.emit('line_disconnected', { this.socketIO.emit('line_disconnected', {
@ -178,8 +190,9 @@ export default class LineConnection {
appendLog( appendLog(
cleanData(message), cleanData(message),
this.config.stationId, this.config.stationId,
this.config.lineNumber, this.config.stationName,
this.config.port this.config.stationIp,
this.config.lineNumber
) )
console.log(`⏳ Connection timeout line ${lineNumber}`) console.log(`⏳ Connection timeout line ${lineNumber}`)
this.client.destroy() this.client.destroy()
@ -257,8 +270,9 @@ export default class LineConnection {
appendLog( appendLog(
`\n\n---start-scenarios---${now}---${userName}---\n---scenario---${script?.title}---${now}---\n`, `\n\n---start-scenarios---${now}---${userName}---\n---scenario---${script?.title}---${now}---\n`,
this.config.stationId, this.config.stationId,
this.config.lineNumber, this.config.stationName,
this.config.port this.config.stationIp,
this.config.lineNumber
) )
this.config.latestScenario = { this.config.latestScenario = {
name: script?.title, name: script?.title,
@ -282,8 +296,9 @@ export default class LineConnection {
appendLog( appendLog(
`\n---end-scenarios---${now}---${userName}---\n`, `\n---end-scenarios---${now}---${userName}---\n`,
this.config.stationId, this.config.stationId,
this.config.lineNumber, this.config.stationName,
this.config.port this.config.stationIp,
this.config.lineNumber
) )
// reject(new Error('Script timeout')) // reject(new Error('Script timeout'))
}, script.timeout || 300000) }, script.timeout || 300000)
@ -303,8 +318,9 @@ export default class LineConnection {
appendLog( appendLog(
`\n---end-scenarios---${now}---${userName}---\n`, `\n---end-scenarios---${now}---${userName}---\n`,
this.config.stationId, this.config.stationId,
this.config.lineNumber, this.config.stationName,
this.config.port this.config.stationIp,
this.config.lineNumber
) )
const logScenarios = getLogWithTimeScenario(this.outputScenario, now) || '' const logScenarios = getLogWithTimeScenario(this.outputScenario, now) || ''
@ -342,8 +358,9 @@ export default class LineConnection {
appendLog( appendLog(
`\n---send-command---"${step?.send ?? ''}"---${now}---\n`, `\n---send-command---"${step?.send ?? ''}"---${now}---\n`,
this.config.stationId, this.config.stationId,
this.config.lineNumber, this.config.stationName,
this.config.port this.config.stationIp,
this.config.lineNumber
) )
let repeatCount = Number(step.repeat) || 1 let repeatCount = Number(step.repeat) || 1
const sendCommand = async () => { const sendCommand = async () => {
@ -396,8 +413,9 @@ export default class LineConnection {
appendLog( appendLog(
`\n-------${user.userName}-------\n`, `\n-------${user.userName}-------\n`,
this.config.stationId, this.config.stationId,
this.config.lineNumber, this.config.stationName,
this.config.port this.config.stationIp,
this.config.lineNumber
) )
} }
@ -457,8 +475,9 @@ export default class LineConnection {
appendLog( appendLog(
cleanData(data), cleanData(data),
this.config.stationId, this.config.stationId,
this.config.lineNumber, this.config.stationName,
this.config.port this.config.stationIp,
this.config.lineNumber
) )
}, },
}) })

View File

@ -33,6 +33,7 @@ export default class SwitchController {
public ports: PortInfo[] public ports: PortInfo[]
public portGroups: PortInfo[][] public portGroups: PortInfo[][]
private isEnable: boolean private isEnable: boolean
private retryConnect: number
constructor({ host, port = 23, username, password, onData }: SwitchControllerOptions) { constructor({ host, port = 23, username, password, onData }: SwitchControllerOptions) {
this.host = host this.host = host
@ -47,6 +48,7 @@ export default class SwitchController {
this.ports = [] this.ports = []
this.portGroups = [] this.portGroups = []
this.isEnable = false this.isEnable = false
this.retryConnect = 0
} }
private sleep(ms: number) { private sleep(ms: number) {
@ -65,10 +67,17 @@ export default class SwitchController {
} }
} }
private _handleClose() { private async _handleClose(err: boolean) {
console.log('[SWITCH CONNECTION CLOSE]', err)
this.status = 'DISCONNECTED' this.status = 'DISCONNECTED'
this.isEnable = false this.isEnable = false
this.onData(this.portGroups, this.status) this.onData(this.portGroups, this.status)
if (this.retryConnect <= 5) {
await this.sleep(15000)
console.log('Retry connect times', this.retryConnect)
this.retryConnect += 1
await this.reconnect()
}
} }
private _handleError(err: Error & { code?: string }) { private _handleError(err: Error & { code?: string }) {
@ -113,8 +122,8 @@ export default class SwitchController {
this.socket.on('data', (data) => this._handleData(data.toString())) this.socket.on('data', (data) => this._handleData(data.toString()))
resolve() resolve()
}) })
this.socket.on('close', () => { this.socket.on('close', (e) => {
this._handleClose() this._handleClose(e)
resolve() resolve()
}) })
this.socket.on('error', (err) => { this.socket.on('error', (err) => {

View File

@ -31,10 +31,20 @@ export function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms)) return new Promise((resolve) => setTimeout(resolve, ms))
} }
export function appendLog(output: string, stationId: number, lineNumber: number, port: number) { // 20250527-AUTO-Session.Station_1-13-192.168.171.9-2.log
// {DATE}-AUTO-Session.{Station name}-{Station ID}-{Station IP}-{Line number}.log
export function appendLog(
output: string,
stationId: number,
stationName: string,
stationIP: string,
lineNumber: number
) {
const date = new Date().toISOString().slice(0, 10).replace(/-/g, '') // YYYYMMDD const date = new Date().toISOString().slice(0, 10).replace(/-/g, '') // YYYYMMDD
const logDir = path.join('storage', 'system_logs') const logDir = path.join('storage', 'system_logs')
const logFile = path.join(logDir, `${date}-Station_${stationId}-Line_${lineNumber}_${port}.log`) const logFile = path
.join(logDir, `${date}-AUTO-Session.${stationName}-${stationId}-${stationIP}-${lineNumber}.log`)
.replaceAll(' ', '_')
// Ensure folder exists // Ensure folder exists
if (!fs.existsSync(logDir)) { if (!fs.existsSync(logDir)) {

View File

@ -148,7 +148,7 @@ export class WebSocketIo {
lineIds, lineIds,
async (line) => async (line) =>
command === 'spam_break' ? line.breakSpam() : line.writeCommand(command, userName), command === 'spam_break' ? line.breakSpam() : line.writeCommand(command, userName),
{ command, timeout: 120000 } { command }
) )
}) })
@ -162,7 +162,6 @@ export class WebSocketIo {
async (line) => line.runScript(scenario, userName), async (line) => line.runScript(scenario, userName),
{ {
scenario, scenario,
timeout: scenario?.timeout ? Number(scenario.timeout) + 120000 : 300000,
} }
) )
}) })
@ -203,7 +202,7 @@ export class WebSocketIo {
stationId, stationId,
[lineId], [lineId],
async (lineCon) => lineCon.writeCommand('\r\n', userName), async (lineCon) => lineCon.writeCommand('\r\n', userName),
{ command: '\r\n', timeout: 120000 } { command: '\r\n' }
) )
} else { } else {
if (this.lineConnecting.includes(lineId)) return if (this.lineConnecting.includes(lineId)) return
@ -322,7 +321,7 @@ export class WebSocketIo {
.andWhere('outlet', outletNumber) .andWhere('outlet', outletNumber)
if (lines.length > 0) { if (lines.length > 0) {
const line = this.lineMap.get(lines[0].id) const line = this.lineMap.get(lines[0].id)
if (line) this.setTimeoutConnect(lines[0].id, line, 300000) if (line) this.setTimeoutConnect(lines[0].id, line)
} }
const apcIp = (station as any)[`${apcName}_ip`] as string const apcIp = (station as any)[`${apcName}_ip`] as string
@ -508,6 +507,8 @@ export class WebSocketIo {
ip: station.ip, ip: station.ip,
lineNumber: line.lineNumber, lineNumber: line.lineNumber,
stationId: station.id, stationId: station.id,
stationName: station.name,
stationIp: station.ip,
apcName: line.apcName, apcName: line.apcName,
outlet: line.outlet, outlet: line.outlet,
baud: line.baud, baud: line.baud,
@ -542,7 +543,7 @@ export class WebSocketIo {
private setTimeoutConnect = ( private setTimeoutConnect = (
lineId: number, lineId: number,
lineConn: LineConnection | SwitchController, lineConn: LineConnection | SwitchController,
timeout = 120000 timeout = 28800000 // 8h = 8*60*60*1000
) => { ) => {
if (this.intervalMap[`${lineId}`]) { if (this.intervalMap[`${lineId}`]) {
clearInterval(this.intervalMap[`${lineId}`]) clearInterval(this.intervalMap[`${lineId}`])
@ -576,7 +577,7 @@ export class WebSocketIo {
// console.log(line?.config) // console.log(line?.config)
if (line && line.config.status === 'connected') { if (line && line.config.status === 'connected') {
this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId) this.lineConnecting = this.lineConnecting.filter((el) => el !== lineId)
this.setTimeoutConnect(lineId, line, options.timeout) this.setTimeoutConnect(lineId, line)
// await sleep(500) // await sleep(500)
await action(line, options) await action(line, options)
} else { } else {
@ -598,7 +599,7 @@ export class WebSocketIo {
const lineReconnect = this.lineMap.get(lineId) const lineReconnect = this.lineMap.get(lineId)
if (lineReconnect) { if (lineReconnect) {
this.setTimeoutConnect(lineId, lineReconnect, options.timeout) this.setTimeoutConnect(lineId, lineReconnect)
await sleep(100) await sleep(100)
await action(lineReconnect, options) await action(lineReconnect, options)
} }

View File

@ -4,7 +4,14 @@ import "@mantine/notifications/styles.css";
import "./App.css"; import "./App.css";
import classes from "./App.module.css"; import classes from "./App.module.css";
import { Suspense, useCallback, useEffect, useMemo, useState } from "react"; import {
Suspense,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { import {
Tabs, Tabs,
Text, Text,
@ -79,6 +86,8 @@ function App() {
const [isLogModalOpen, setIsLogModalOpen] = useState(false); const [isLogModalOpen, setIsLogModalOpen] = useState(false);
const [expandedBottomBar, setExpandedBottomBar] = useState(true); const [expandedBottomBar, setExpandedBottomBar] = useState(true);
const [activeTabBottom, setActiveTabBottom] = useState<string>("command"); const [activeTabBottom, setActiveTabBottom] = useState<string>("command");
const lineBuffersRef = useRef(new Map<number, string>());
const flushScheduledRef = useRef(false);
const connectApcSwitch = (station: TStation) => { const connectApcSwitch = (station: TStation) => {
if (station?.apc_1_ip && station?.apc_1_port) { if (station?.apc_1_ip && station?.apc_1_port) {
@ -165,11 +174,20 @@ function App() {
); );
socket?.on("line_output", (data) => { socket?.on("line_output", (data) => {
updateValueLineStation( const { lineId, data: text } = data;
data?.lineId, // updateValueLineStation(
{ netOutput: data.data, commands: data.commands || [] }, // data?.lineId,
data?.stationId // { netOutput: data.data, commands: data.commands || [] },
); // data?.stationId
// );
const buf = lineBuffersRef.current.get(lineId) || "";
lineBuffersRef.current.set(lineId, buf + text);
if (!flushScheduledRef.current) {
flushScheduledRef.current = true;
setTimeout(() => flushBuffers(), 50);
}
}); });
socket?.on("line_error", (data) => { socket?.on("line_error", (data) => {
@ -323,6 +341,28 @@ function App() {
}; };
}, [socket, stations, selectedLine]); }, [socket, stations, selectedLine]);
const flushBuffers = useCallback(() => {
setStations((prev) =>
prev.map((station) => ({
...station,
lines: station.lines.map((line) => {
const buffered = lineBuffersRef.current.get(line.id || 0);
if (!buffered) return line; // không có update
updateValueSelectedLine(line?.id || 0, { netOutput: buffered });
return {
...line,
netOutput: (line.netOutput || "") + buffered,
output: buffered,
loadingOutput: line.loadingOutput ? false : true,
};
}),
}))
);
// clear
lineBuffersRef.current.clear();
flushScheduledRef.current = false;
}, []);
const updateValueLineStation = useCallback( const updateValueLineStation = useCallback(
(lineId: number, updates: Partial<TLine>, stationId?: number) => { (lineId: number, updates: Partial<TLine>, stationId?: number) => {
setStations((prevStations) => setStations((prevStations) =>
@ -374,6 +414,29 @@ function App() {
[] []
); );
const updateValueSelectedLine = useCallback(
(lineId: number, updates: Partial<TLine>) => {
// Update selectedLine nếu nó đang được chọn
setSelectedLine((prevSelected) => {
if (!prevSelected || prevSelected.id !== lineId) return prevSelected;
const isNetOutput = typeof updates?.netOutput !== "undefined";
return {
...prevSelected,
...updates,
...(isNetOutput && {
netOutput:
(prevSelected.netOutput || "") + (updates.netOutput || ""),
output: updates.netOutput,
loadingOutput: prevSelected.loadingOutput ? false : true,
}),
};
});
},
[]
);
// const getLine = (lineId: number, stationId: number) => { // const getLine = (lineId: number, stationId: number) => {
// const station = stations?.find((sta) => sta.id === stationId); // const station = stations?.find((sta) => sta.id === stationId);
// if (station) { // if (station) {
@ -530,7 +593,6 @@ function App() {
onChange={(id) => { onChange={(id) => {
if (selectedLines.length > 0) { if (selectedLines.length > 0) {
selectedLines.forEach((el) => { selectedLines.forEach((el) => {
console.log(el?.userOpenCLI, user?.userName);
if (el?.userOpenCLI === user?.userName) if (el?.userOpenCLI === user?.userName)
socket?.emit("close_cli", { socket?.emit("close_cli", {
lineId: el?.id, lineId: el?.id,

View File

@ -98,7 +98,8 @@ function DrawerLogs({
<div> <div>
Format: Format:
<i style={{ marginLeft: "4px" }}> <i style={{ marginLeft: "4px" }}>
YYYYMMDD-Station_{`{id}`}-Line_{`{number}`}_{`{port}`} YYYYMMDD-AUTO-Session.{`{Station name}`}-{`{Station ID}`}-
{`{Station IP}`}-{`{Line number}`}
.log .log
</i> </i>
</div> </div>

View File

@ -146,8 +146,6 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
setLoading(false); setLoading(false);
}, 500); }, 500);
if (fitRef.current) fitRef.current?.fit(); if (fitRef.current) fitRef.current?.fit();
} else {
setIsInit(false);
} }
}, [cliOpened]); }, [cliOpened]);