Update
This commit is contained in:
parent
077a2ddc35
commit
ef1d585b61
|
|
@ -1,5 +1,6 @@
|
|||
import net from 'node:net'
|
||||
import { cleanData } from '../ultils/helper.js'
|
||||
import Scenario from '#models/scenario'
|
||||
|
||||
interface LineConfig {
|
||||
id: number
|
||||
|
|
@ -16,11 +17,17 @@ 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) {
|
||||
|
|
@ -35,18 +42,24 @@ export default class LineConnection {
|
|||
resolvedOrRejected = true
|
||||
|
||||
console.log(`✅ Connected to line ${lineNumber} (${ip}:${port})`)
|
||||
this.config.status = 'connected'
|
||||
this.socketIO.emit('line_connected', {
|
||||
stationId,
|
||||
lineId: id,
|
||||
lineNumber,
|
||||
status: 'connected',
|
||||
})
|
||||
resolve()
|
||||
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()
|
||||
this.outputBuffer += message
|
||||
// let output = cleanData(message)
|
||||
// console.log(`📨 [${this.config.port}] ${message}`)
|
||||
// Handle netOutput with backspace support
|
||||
|
|
@ -74,7 +87,7 @@ export default class LineConnection {
|
|||
this.socketIO.emit('line_error', {
|
||||
stationId,
|
||||
lineId: id,
|
||||
error: err.message,
|
||||
error: err.message + '\r\n',
|
||||
})
|
||||
reject(err)
|
||||
})
|
||||
|
|
@ -101,15 +114,6 @@ export default class LineConnection {
|
|||
})
|
||||
}
|
||||
|
||||
sendCommand(cmd: string) {
|
||||
if (this.client.destroyed) {
|
||||
console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`)
|
||||
return
|
||||
}
|
||||
// console.log(`➡️ [${this.config.apcName}] SEND:`, cmd)
|
||||
this.client.write(`${cmd}\r\n`)
|
||||
}
|
||||
|
||||
writeCommand(cmd: string) {
|
||||
if (this.client.destroyed) {
|
||||
console.log(`⚠️ Cannot send, line ${this.config.lineNumber} is closed`)
|
||||
|
|
@ -131,4 +135,75 @@ export default class LineConnection {
|
|||
console.error('Error closing line:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async runScript(script: Scenario) {
|
||||
if (!this.client || this.client.destroyed) {
|
||||
throw new Error('Not connected')
|
||||
}
|
||||
if (this.isRunningScript) {
|
||||
throw new Error('Script already running')
|
||||
}
|
||||
|
||||
this.isRunningScript = true
|
||||
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 = ''
|
||||
reject(new Error('Script timeout'))
|
||||
}, script.timeout || 300000)
|
||||
|
||||
const runStep = (index: number) => {
|
||||
if (index >= steps.length) {
|
||||
clearTimeout(timeoutTimer)
|
||||
this.isRunningScript = false
|
||||
this.outputBuffer = ''
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
|
||||
const step = steps[index]
|
||||
let repeatCount = Number(step.repeat) || 1
|
||||
|
||||
const sendCommand = () => {
|
||||
if (repeatCount <= 0) {
|
||||
// Done → next step
|
||||
this.client.off('data', onOutput)
|
||||
stepIndex++
|
||||
return runStep(stepIndex)
|
||||
}
|
||||
|
||||
if (step.send) {
|
||||
this.writeCommand(step?.send + '\r\n')
|
||||
}
|
||||
|
||||
repeatCount--
|
||||
setTimeout(() => sendCommand(), Number(step?.delay) || 500)
|
||||
}
|
||||
|
||||
// Lắng nghe output cho expect
|
||||
const onOutput = (data: string) => {
|
||||
const output = data.toString()
|
||||
this.outputBuffer += output
|
||||
|
||||
if (output.includes(step.expect)) {
|
||||
this.client.off('data', onOutput)
|
||||
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
|
||||
}
|
||||
|
||||
this.client.on('data', onOutput)
|
||||
}
|
||||
|
||||
runStep(stepIndex)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* @param {string} data - The raw data to be cleaned.
|
||||
* @returns {string} - The cleaned data.
|
||||
*/
|
||||
export const cleanData = (data) => {
|
||||
export const cleanData = (data: string) => {
|
||||
return data
|
||||
.replace(/--More--\s*BS\s*BS\s*BS\s*BS\s*BS\s*BS/g, '')
|
||||
.replace(/\s*--More--\s*/g, '')
|
||||
|
|
|
|||
|
|
@ -5,13 +5,7 @@ import { ApplicationService } from '@adonisjs/core/types'
|
|||
import env from '#start/env'
|
||||
import { CustomServer, CustomSocket } from '../app/ultils/types.js'
|
||||
import Line from '#models/line'
|
||||
|
||||
interface Station {
|
||||
id: number
|
||||
name: string
|
||||
ip: string
|
||||
lines: any[]
|
||||
}
|
||||
import Station from '#models/station'
|
||||
|
||||
export default class SocketIoProvider {
|
||||
private static _io: CustomServer
|
||||
|
|
@ -78,11 +72,12 @@ export class WebSocketIo {
|
|||
console.log('Socket connected:', socket.id)
|
||||
socket.connectionTime = new Date()
|
||||
|
||||
const lineConnectionArray: LineConnection[] = Array.from(this.lineMap.values())
|
||||
io.to(socket.id).emit(
|
||||
'init',
|
||||
lineConnectionArray.map((el) => el.config)
|
||||
)
|
||||
setTimeout(() => {
|
||||
io.to(socket.id).emit(
|
||||
'init',
|
||||
Array.from(this.lineMap.values()).map((el) => el.config)
|
||||
)
|
||||
}, 200)
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log(`FE disconnected: ${socket.id}`)
|
||||
|
|
@ -94,16 +89,59 @@ export class WebSocketIo {
|
|||
await this.connectLine(io, linesData, stationData)
|
||||
})
|
||||
|
||||
socket.on('write_command_line_from_web', (data) => {
|
||||
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) {
|
||||
this.setTimeoutConnect(lineId, line)
|
||||
line.writeCommand(command)
|
||||
} else {
|
||||
const linesData = await Line.findBy('id', lineId)
|
||||
const stationData = await Station.findBy('id', stationId)
|
||||
if (linesData && stationData) {
|
||||
await this.connectLine(io, [linesData], stationData)
|
||||
const lineReconnect = this.lineMap.get(lineId)
|
||||
if (lineReconnect) {
|
||||
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) {
|
||||
this.setTimeoutConnect(
|
||||
lineId,
|
||||
line,
|
||||
scenario?.timeout ? Number(scenario?.timeout) + 120000 : 300000
|
||||
)
|
||||
line.runScript(scenario)
|
||||
} else {
|
||||
const linesData = await Line.findBy('id', lineId)
|
||||
const stationData = await Station.findBy('id', data.stationId)
|
||||
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,
|
||||
stationId: data.stationId,
|
||||
lineId,
|
||||
status: 'disconnected',
|
||||
})
|
||||
|
|
@ -168,7 +206,7 @@ export class WebSocketIo {
|
|||
console.log(`🔻 Station ${station.name} disconnected`)
|
||||
}
|
||||
|
||||
private setTimeoutConnect = (lineId: number, lineConn: LineConnection) => {
|
||||
private setTimeoutConnect = (lineId: number, lineConn: LineConnection, timeout = 120000) => {
|
||||
if (this.intervalMap[`${lineId}`]) {
|
||||
clearInterval(this.intervalMap[`${lineId}`])
|
||||
delete this.intervalMap[`${lineId}`]
|
||||
|
|
@ -176,7 +214,11 @@ export class WebSocketIo {
|
|||
const interval = setInterval(() => {
|
||||
lineConn.disconnect()
|
||||
this.lineMap.delete(lineId)
|
||||
}, 120000)
|
||||
if (this.intervalMap[`${lineId}`]) {
|
||||
clearInterval(this.intervalMap[`${lineId}`])
|
||||
delete this.intervalMap[`${lineId}`]
|
||||
}
|
||||
}, timeout)
|
||||
|
||||
this.intervalMap[`${lineId}`] = interval
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@ import {
|
|||
Button,
|
||||
ActionIcon,
|
||||
} from "@mantine/core";
|
||||
import type { TLine, TStation } from "./untils/types";
|
||||
import type { LineConfig, TLine, TStation } from "./untils/types";
|
||||
import axios from "axios";
|
||||
import CardLine from "./components/CardLine";
|
||||
import { IconEdit, IconSettingsPlus } from "@tabler/icons-react";
|
||||
import { SocketProvider, useSocket } from "./context/SocketContext";
|
||||
import { ButtonDPELP } from "./components/ButtonAction";
|
||||
|
||||
const apiUrl = import.meta.env.VITE_BACKEND_URL;
|
||||
|
||||
|
|
@ -43,6 +44,7 @@ export function App() {
|
|||
setControlsRefs(controlsRefs);
|
||||
};
|
||||
const [showBottomShadow, setShowBottomShadow] = useState(false);
|
||||
const [isDisable, setIsDisable] = useState(false);
|
||||
|
||||
// function get list station
|
||||
const getStation = async () => {
|
||||
|
|
@ -67,35 +69,27 @@ export function App() {
|
|||
useEffect(() => {
|
||||
if (!socket || !stations?.length) return;
|
||||
|
||||
const updateStatus = (data: any) => {
|
||||
const line = getLine(data.lineId, data.stationId);
|
||||
if (line) {
|
||||
updateValueLineStation(line, "status", data.status);
|
||||
}
|
||||
};
|
||||
|
||||
socket.on("line_connected", updateStatus);
|
||||
socket.on("line_disconnected", updateStatus);
|
||||
socket?.on("init", (data) => {
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach((value) => {
|
||||
updateStatus(value);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ cleanup on unmount or when socket changes
|
||||
return () => {
|
||||
socket.off("line_connected");
|
||||
socket.off("line_disconnected");
|
||||
socket.off("line_disconnected");
|
||||
};
|
||||
}, [socket, stations]);
|
||||
|
||||
const updateValueLineStation = (
|
||||
const updateStatus = (data: LineConfig) => {
|
||||
const line = getLine(data.lineId, data.stationId);
|
||||
if (line) {
|
||||
updateValueLineStation(line, "status", data.status);
|
||||
}
|
||||
};
|
||||
|
||||
const updateValueLineStation = <K extends keyof TLine>(
|
||||
currentLine: TLine,
|
||||
field: string,
|
||||
value: any
|
||||
field: K,
|
||||
value: TLine[K]
|
||||
) => {
|
||||
setStations((el) =>
|
||||
el?.map((station: TStation) =>
|
||||
|
|
@ -201,6 +195,7 @@ export function App() {
|
|||
line={line}
|
||||
selectedLines={selectedLines}
|
||||
setSelectedLines={setSelectedLines}
|
||||
updateStatus={updateStatus}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
|
|
@ -223,7 +218,7 @@ export function App() {
|
|||
>
|
||||
<Button
|
||||
variant="filled"
|
||||
style={{ height: "30px", width: "120px" }}
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {
|
||||
if (selectedLines.length !== station.lines.length)
|
||||
setSelectedLines(station.lines);
|
||||
|
|
@ -240,7 +235,7 @@ export function App() {
|
|||
.length === 0
|
||||
}
|
||||
variant="outline"
|
||||
style={{ height: "30px", width: "120px" }}
|
||||
style={{ height: "30px", width: "100px" }}
|
||||
onClick={() => {
|
||||
const lines = selectedLines.filter(
|
||||
(el) => el.status !== "connected"
|
||||
|
|
@ -254,6 +249,18 @@ export function App() {
|
|||
>
|
||||
Connect
|
||||
</Button>
|
||||
<ButtonDPELP
|
||||
socket={socket}
|
||||
selectedLines={selectedLines}
|
||||
isDisable={isDisable || selectedLines.length === 0}
|
||||
onClick={() => {
|
||||
setSelectedLines([]);
|
||||
setIsDisable(true);
|
||||
setTimeout(() => {
|
||||
setIsDisable(false);
|
||||
}, 10000);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
import type { Socket } from "socket.io-client";
|
||||
import type { TLine } from "../untils/types";
|
||||
import { Button } from "@mantine/core";
|
||||
|
||||
export const ButtonDPELP = ({
|
||||
socket,
|
||||
isDisable,
|
||||
onClick,
|
||||
selectedLines,
|
||||
}: {
|
||||
socket: Socket | null;
|
||||
isDisable: boolean;
|
||||
onClick: () => void;
|
||||
selectedLines: TLine[];
|
||||
}) => {
|
||||
return (
|
||||
<Button
|
||||
disabled={isDisable}
|
||||
miw={"100px"}
|
||||
// radius="lg"
|
||||
h={"24px"}
|
||||
mr={"5px"}
|
||||
variant="filled"
|
||||
color="#00a164"
|
||||
onClick={async () => {
|
||||
onClick();
|
||||
selectedLines?.forEach((el) => {
|
||||
const body = [
|
||||
{
|
||||
expect: "",
|
||||
send: " show diag",
|
||||
delay: "1000",
|
||||
repeat: "1",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
expect: "",
|
||||
send: " ",
|
||||
delay: "1000",
|
||||
repeat: "1",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
expect: "",
|
||||
send: " show post",
|
||||
delay: "1000",
|
||||
repeat: "1",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
expect: "",
|
||||
send: " ",
|
||||
delay: "1000",
|
||||
repeat: "1",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
expect: "",
|
||||
send: " show env",
|
||||
delay: "1000",
|
||||
repeat: "1",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
expect: "",
|
||||
send: " ",
|
||||
delay: "1000",
|
||||
repeat: "1",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
expect: "",
|
||||
send: " show license",
|
||||
delay: "1000",
|
||||
repeat: "1",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
expect: "",
|
||||
send: " ",
|
||||
delay: "1000",
|
||||
repeat: "1",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
expect: "",
|
||||
send: " show log",
|
||||
delay: "1000",
|
||||
repeat: "1",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
expect: "",
|
||||
send: " ",
|
||||
delay: "1000",
|
||||
repeat: "2",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
expect: "",
|
||||
send: " show platform",
|
||||
delay: "1000",
|
||||
repeat: "1",
|
||||
note: "",
|
||||
},
|
||||
{
|
||||
expect: "",
|
||||
send: " ",
|
||||
delay: "7000",
|
||||
repeat: "15",
|
||||
note: "",
|
||||
},
|
||||
];
|
||||
socket?.emit(
|
||||
"run_scenario",
|
||||
Object.assign(el, {
|
||||
scenario: {
|
||||
id: 0,
|
||||
is_reboot: 0,
|
||||
title: "DPELP",
|
||||
timeout: 300000,
|
||||
body: JSON.stringify(body),
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
}}
|
||||
>
|
||||
DPELP
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Card, Text, Box, Flex } from "@mantine/core";
|
||||
import type { TLine, TStation } from "../untils/types";
|
||||
import type { LineConfig, TLine, TStation } from "../untils/types";
|
||||
import classes from "./Component.module.css";
|
||||
import TerminalCLI from "./TerminalXTerm";
|
||||
import type { Socket } from "socket.io-client";
|
||||
|
|
@ -12,12 +12,14 @@ const CardLine = ({
|
|||
setSelectedLines,
|
||||
socket,
|
||||
stationItem,
|
||||
updateStatus,
|
||||
}: {
|
||||
line: TLine;
|
||||
selectedLines: TLine[];
|
||||
setSelectedLines: (lines: React.SetStateAction<TLine[]>) => void;
|
||||
socket: Socket | null;
|
||||
stationItem: TStation;
|
||||
updateStatus: (value: LineConfig) => void;
|
||||
}) => {
|
||||
return (
|
||||
<Card
|
||||
|
|
@ -81,6 +83,7 @@ const CardLine = ({
|
|||
paddingBottom: "0px",
|
||||
}}
|
||||
onDoubleClick={() => {}}
|
||||
updateStatus={updateStatus}
|
||||
/>
|
||||
</Box>
|
||||
</Flex>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import "xterm/css/xterm.css";
|
|||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { SOCKET_EVENTS } from "../untils/constanst";
|
||||
import type { Socket } from "socket.io-client";
|
||||
import type { LineConfig } from "../untils/types";
|
||||
|
||||
interface TerminalCLIProps {
|
||||
socket: Socket | null;
|
||||
|
|
@ -24,6 +25,7 @@ interface TerminalCLIProps {
|
|||
onDoubleClick?: () => void;
|
||||
fontSize?: number;
|
||||
miniSize?: boolean;
|
||||
updateStatus: (value: LineConfig) => void;
|
||||
}
|
||||
|
||||
const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
||||
|
|
@ -33,11 +35,11 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
station_id,
|
||||
cliOpened = false,
|
||||
isDisabled = false,
|
||||
line_status = "",
|
||||
customStyle = {},
|
||||
onDoubleClick = () => {},
|
||||
fontSize = 14,
|
||||
miniSize = false,
|
||||
updateStatus,
|
||||
}) => {
|
||||
const xtermRef = useRef<HTMLDivElement>(null);
|
||||
const terminal = useRef<Terminal>(null);
|
||||
|
|
@ -148,6 +150,7 @@ const TerminalCLI: React.FC<TerminalCLIProps> = ({
|
|||
data.forEach((value) => {
|
||||
if (value?.id === line_id && terminal.current) {
|
||||
terminal.current?.write(value.output);
|
||||
updateStatus({ ...value, lineId: value.id });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,3 +126,15 @@ export type SwitchPortsProps = {
|
|||
status: string;
|
||||
poe: string;
|
||||
};
|
||||
|
||||
export type LineConfig = {
|
||||
id: number;
|
||||
lineId: number;
|
||||
port: number;
|
||||
lineNumber: number;
|
||||
ip: string;
|
||||
stationId: number;
|
||||
apcName?: string;
|
||||
output: string;
|
||||
status: string;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue