Refactor log handling and improve output buffering #5

Merged
andrew.ng merged 1 commits from that into main 2025-11-25 15:32:25 +11:00
7 changed files with 140 additions and 40 deletions

View File

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

View File

@ -33,6 +33,7 @@ export default class SwitchController {
public ports: PortInfo[]
public portGroups: PortInfo[][]
private isEnable: boolean
private retryConnect: number
constructor({ host, port = 23, username, password, onData }: SwitchControllerOptions) {
this.host = host
@ -47,6 +48,7 @@ export default class SwitchController {
this.ports = []
this.portGroups = []
this.isEnable = false
this.retryConnect = 0
}
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.isEnable = false
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 }) {
@ -113,8 +122,8 @@ export default class SwitchController {
this.socket.on('data', (data) => this._handleData(data.toString()))
resolve()
})
this.socket.on('close', () => {
this._handleClose()
this.socket.on('close', (e) => {
this._handleClose(e)
resolve()
})
this.socket.on('error', (err) => {

View File

@ -31,10 +31,20 @@ export function sleep(ms: number) {
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 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
if (!fs.existsSync(logDir)) {

View File

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

View File

@ -4,7 +4,14 @@ import "@mantine/notifications/styles.css";
import "./App.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 {
Tabs,
Text,
@ -79,6 +86,8 @@ function App() {
const [isLogModalOpen, setIsLogModalOpen] = useState(false);
const [expandedBottomBar, setExpandedBottomBar] = useState(true);
const [activeTabBottom, setActiveTabBottom] = useState<string>("command");
const lineBuffersRef = useRef(new Map<number, string>());
const flushScheduledRef = useRef(false);
const connectApcSwitch = (station: TStation) => {
if (station?.apc_1_ip && station?.apc_1_port) {
@ -165,11 +174,20 @@ function App() {
);
socket?.on("line_output", (data) => {
updateValueLineStation(
data?.lineId,
{ netOutput: data.data, commands: data.commands || [] },
data?.stationId
);
const { lineId, data: text } = data;
// updateValueLineStation(
// data?.lineId,
// { 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) => {
@ -323,6 +341,28 @@ function App() {
};
}, [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(
(lineId: number, updates: Partial<TLine>, stationId?: number) => {
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 station = stations?.find((sta) => sta.id === stationId);
// if (station) {
@ -530,7 +593,6 @@ function App() {
onChange={(id) => {
if (selectedLines.length > 0) {
selectedLines.forEach((el) => {
console.log(el?.userOpenCLI, user?.userName);
if (el?.userOpenCLI === user?.userName)
socket?.emit("close_cli", {
lineId: el?.id,

View File

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

View File

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