feat(database): add CRUD operations for parsed sessions and update session name functionality
This commit is contained in:
17
README.md
17
README.md
@@ -58,16 +58,18 @@ npm install
|
|||||||
cd ..
|
cd ..
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **开发模式运行**
|
4. **开发模式运行(推荐)**
|
||||||
```bash
|
```bash
|
||||||
make dev
|
wails dev
|
||||||
```
|
```
|
||||||
|
|
||||||
5. **构建发布版本**
|
5. **构建发布版本**
|
||||||
```bash
|
```bash
|
||||||
make release
|
wails build -clean
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 说明:在 Windows 下可以直接使用 `wails dev` 进入开发模式,不依赖 `make`。
|
||||||
|
|
||||||
## 使用说明
|
## 使用说明
|
||||||
|
|
||||||
### 1. 启动应用
|
### 1. 启动应用
|
||||||
@@ -209,14 +211,13 @@ make lint
|
|||||||
### 构建
|
### 构建
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 开发构建
|
# 开发模式
|
||||||
make build
|
wails dev
|
||||||
|
|
||||||
# 发布构建
|
# 发布构建
|
||||||
make release
|
wails build -clean
|
||||||
|
|
||||||
# 清理构建文件
|
# (可选)如果你本地装了 make,也可以使用 make build / make release / make clean
|
||||||
make clean
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 数据存储
|
## 数据存储
|
||||||
|
|||||||
1205
frontend/package-lock.json
generated
1205
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
|||||||
98e3c997ed559906a235af48ee6be17d
|
340ef73da49fc2e1f0f32752fcad55cd
|
||||||
@@ -1,24 +1,19 @@
|
|||||||
import React, {useEffect, useRef, useState} from 'react';
|
import React, {useEffect, useRef, useState} from 'react';
|
||||||
import {Button, Card, Layout, message, Select, Spin, Table} from 'antd';
|
import {Button, Card, Layout, message, Spin, Table} from 'antd';
|
||||||
import {DownloadOutlined, PlayCircleOutlined, StopOutlined, UploadOutlined} from '@ant-design/icons';
|
import {DownloadOutlined, PlayCircleOutlined, StopOutlined, UploadOutlined} from '@ant-design/icons';
|
||||||
import '../App.css';
|
import '../App.css';
|
||||||
import {
|
import {
|
||||||
GetNetworkInterfaces,
|
|
||||||
ReadRawJsonFile,
|
ReadRawJsonFile,
|
||||||
SaveParsedDataToDatabase,
|
SaveParsedDataToDatabase,
|
||||||
StartCapture,
|
StartCaptureWithFilter,
|
||||||
|
StopCapture,
|
||||||
StopAndParseCapture
|
StopAndParseCapture
|
||||||
} from '../../wailsjs/go/service/App';
|
} from '../../wailsjs/go/service/App';
|
||||||
|
import { GetCaptureStatus } from '../../wailsjs/go/service/App';
|
||||||
|
import { EventsOn } from '../../wailsjs/runtime';
|
||||||
import {useCaptureStore} from '../store/useCaptureStore';
|
import {useCaptureStore} from '../store/useCaptureStore';
|
||||||
|
|
||||||
const { Header, Content, Sider } = Layout;
|
const { Content, Sider } = Layout;
|
||||||
|
|
||||||
interface NetworkInterface {
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
addresses: string[]
|
|
||||||
is_loopback: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Equipment {
|
interface Equipment {
|
||||||
id: string
|
id: string
|
||||||
@@ -40,21 +35,15 @@ interface CaptureResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CapturePage() {
|
function CapturePage() {
|
||||||
// 只用全局 parsedData
|
|
||||||
const parsedData = useCaptureStore(state => state.parsedData);
|
const parsedData = useCaptureStore(state => state.parsedData);
|
||||||
const setParsedData = useCaptureStore(state => state.setParsedData);
|
const setParsedData = useCaptureStore(state => state.setParsedData);
|
||||||
|
|
||||||
// 其余状态全部本地 useState
|
|
||||||
const [interfaces, setInterfaces] = useState<NetworkInterface[]>([]);
|
|
||||||
const [selectedInterface, setSelectedInterface] = useState('');
|
|
||||||
const [isCapturing, setIsCapturing] = useState(false);
|
const [isCapturing, setIsCapturing] = useState(false);
|
||||||
const [capturedData, setCapturedData] = useState<string[]>([]);
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [interfaceLoading, setInterfaceLoading] = useState(false);
|
|
||||||
const [uploadedFileName, setUploadedFileName] = useState('');
|
const [uploadedFileName, setUploadedFileName] = useState('');
|
||||||
|
const [statusText, setStatusText] = useState('准备就绪');
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
// 顶部 message 只显示一次
|
|
||||||
const showMessage = (type: 'success' | 'error' | 'warning', content: string) => {
|
const showMessage = (type: 'success' | 'error' | 'warning', content: string) => {
|
||||||
message.destroy();
|
message.destroy();
|
||||||
message[type](content);
|
message[type](content);
|
||||||
@@ -73,65 +62,91 @@ function CapturePage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchInterfaces = async () => {
|
const resetUiState = () => {
|
||||||
setIsCapturing(false);
|
setIsCapturing(false);
|
||||||
setCapturedData([]);
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setInterfaceLoading(true);
|
setUploadedFileName('');
|
||||||
try {
|
setParsedData({ items: [], heroes: [] } as CaptureResult);
|
||||||
const response = await safeApiCall(
|
setStatusText('准备就绪');
|
||||||
() => GetNetworkInterfaces(),
|
|
||||||
'获取网络接口失败'
|
|
||||||
);
|
|
||||||
if (!response || response.length === 0) {
|
|
||||||
showMessage('error', '未获取到任何网络接口');
|
|
||||||
setInterfaces([]);
|
|
||||||
setSelectedInterface('');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setInterfaces(response);
|
|
||||||
let defaultSelected = '';
|
|
||||||
console.log("获取的网卡:"+JSON.stringify(response))
|
|
||||||
for (const iface of response) {
|
|
||||||
if (iface.addresses && iface.addresses.some(ip => ip.includes('192.168'))) {
|
|
||||||
defaultSelected = iface.name;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!defaultSelected && response.length > 0) {
|
|
||||||
defaultSelected = response[0].name;
|
|
||||||
}
|
|
||||||
setSelectedInterface(defaultSelected);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取网络接口时发生未知错误:', error);
|
|
||||||
showMessage('error', '获取网络接口时发生未知错误');
|
|
||||||
setInterfaces([]);
|
|
||||||
setSelectedInterface('');
|
|
||||||
} finally {
|
|
||||||
setInterfaceLoading(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
const syncCaptureStatus = async () => {
|
||||||
|
resetUiState();
|
||||||
|
try {
|
||||||
|
const status = await GetCaptureStatus();
|
||||||
|
if (!cancelled && status?.status === 'starting') {
|
||||||
|
setIsCapturing(true);
|
||||||
|
setStatusText('启动中...');
|
||||||
|
} else if (!cancelled && status?.is_capturing) {
|
||||||
|
setIsCapturing(true);
|
||||||
|
setStatusText('抓包中');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// ignore status sync errors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
syncCaptureStatus();
|
||||||
|
return () => {
|
||||||
|
// Leave capture page: ensure backend is not left running
|
||||||
|
StopCapture().catch(() => {});
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const off = EventsOn('capture:ready_to_parse', () => {
|
||||||
|
setStatusText('抓包成功,待解析数据');
|
||||||
|
});
|
||||||
|
const offStarted = EventsOn('capture:started', () => {
|
||||||
|
setIsCapturing(true);
|
||||||
|
setStatusText('抓包中');
|
||||||
|
});
|
||||||
|
const offStartFailed = EventsOn('capture:start_failed', () => {
|
||||||
|
setIsCapturing(false);
|
||||||
|
setStatusText('准备就绪');
|
||||||
|
showMessage('error', '开始抓包失败');
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
off();
|
||||||
|
offStarted();
|
||||||
|
offStartFailed();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const startCapture = async () => {
|
const startCapture = async () => {
|
||||||
if (!selectedInterface) {
|
|
||||||
showMessage('warning', '请选择网络接口');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await safeApiCall(
|
// Best-effort reset to avoid "capture already running"
|
||||||
() => StartCapture(selectedInterface),
|
await StopCapture().catch(() => {});
|
||||||
'开始抓包失败'
|
setParsedData({ items: [], heroes: [] } as CaptureResult);
|
||||||
);
|
setIsCapturing(true);
|
||||||
if (result === undefined) {
|
setStatusText('启动中...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('StartCaptureWithFilter calling', new Date().toISOString());
|
||||||
|
await StartCaptureWithFilter('', '');
|
||||||
|
console.log('StartCaptureWithFilter returned', new Date().toISOString());
|
||||||
|
showMessage('success', '开始抓包');
|
||||||
|
} catch (err) {
|
||||||
|
const msg = String(err);
|
||||||
|
if (msg.includes('capture already running')) {
|
||||||
|
// 后端仍在抓包,允许前端停止
|
||||||
|
setIsCapturing(true);
|
||||||
|
setStatusText('抓包中');
|
||||||
|
showMessage('warning', '抓包已在进行');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsCapturing(false);
|
setIsCapturing(false);
|
||||||
|
setStatusText('准备就绪');
|
||||||
|
showMessage('error', '开始抓包失败');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsCapturing(true);
|
|
||||||
showMessage('success', '开始抓包');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('开始抓包时发生未知错误:', error);
|
console.error('开始抓包出错:', error);
|
||||||
setIsCapturing(false);
|
setIsCapturing(false);
|
||||||
|
setStatusText('准备就绪');
|
||||||
showMessage('error', '开始抓包失败');
|
showMessage('error', '开始抓包失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -139,31 +154,45 @@ function CapturePage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const stopCapture = async () => {
|
const stopCapture = async () => {
|
||||||
|
console.log('stopCapture clicked', new Date().toISOString());
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setIsCapturing(false);
|
setStatusText('抓取中...');
|
||||||
setCapturedData([]);
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// 新接口:直接停止抓包并解析
|
console.log('StopAndParseCapture calling', new Date().toISOString());
|
||||||
const parsedData = await safeApiCall(
|
const parsedData = await StopAndParseCapture();
|
||||||
() => StopAndParseCapture(),
|
console.log('StopAndParseCapture returned', new Date().toISOString());
|
||||||
'停止抓包并解析数据失败'
|
|
||||||
);
|
|
||||||
console.log("解析数据:"+JSON.stringify(parsedData))
|
|
||||||
if (!parsedData || !Array.isArray((parsedData as CaptureResult).items)) {
|
if (!parsedData || !Array.isArray((parsedData as CaptureResult).items)) {
|
||||||
setParsedData({ items: [], heroes: [] } as CaptureResult);
|
setParsedData({ items: [], heroes: [] } as CaptureResult);
|
||||||
showMessage('error', '解析数据失败');
|
setIsCapturing(false);
|
||||||
|
setStatusText('解析失败');
|
||||||
|
showMessage('error', '解析失败');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setParsedData(parsedData as CaptureResult);
|
setParsedData(parsedData as CaptureResult);
|
||||||
|
setIsCapturing(false);
|
||||||
|
setStatusText('导入成功');
|
||||||
showMessage('success', '数据处理完成');
|
showMessage('success', '数据处理完成');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('停止抓包时发生未知错误:', error);
|
const errMsg = String(error);
|
||||||
setIsCapturing(false);
|
console.error('停止抓包出错:', error);
|
||||||
setCapturedData([]);
|
|
||||||
setParsedData({ items: [], heroes: [] } as CaptureResult);
|
setParsedData({ items: [], heroes: [] } as CaptureResult);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
showMessage('error', '抓包失败,已重置状态');
|
if (errMsg.includes('capture starting')) {
|
||||||
|
setIsCapturing(true);
|
||||||
|
setStatusText('启动中,停止已请求');
|
||||||
|
showMessage('warning', '启动中,停止已请求');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (errMsg.includes('no captured data') || errMsg.includes('没有数据')) {
|
||||||
|
setIsCapturing(false);
|
||||||
|
setStatusText('未获取到任何数据');
|
||||||
|
showMessage('error', '未获取到任何数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsCapturing(false);
|
||||||
|
setStatusText('抓包失败');
|
||||||
|
showMessage('error', '抓包失败');
|
||||||
return;
|
return;
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -171,37 +200,26 @@ function CapturePage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const exportData = () => {
|
const exportData = () => {
|
||||||
// 检查是否有解析数据
|
|
||||||
if (!parsedData || !Array.isArray(parsedData.items) || parsedData.items.length === 0) {
|
if (!parsedData || !Array.isArray(parsedData.items) || parsedData.items.length === 0) {
|
||||||
showMessage('warning', '没有数据可导出');
|
showMessage('warning', '没有数据可导出');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 创建要导出的数据内容
|
|
||||||
const exportContent = JSON.stringify(parsedData, null, 2);
|
const exportContent = JSON.stringify(parsedData, null, 2);
|
||||||
|
|
||||||
// 创建 Blob 对象
|
|
||||||
const blob = new Blob([exportContent], { type: 'text/plain;charset=utf-8' });
|
const blob = new Blob([exportContent], { type: 'text/plain;charset=utf-8' });
|
||||||
|
|
||||||
// 创建下载链接
|
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = 'gear.txt';
|
link.download = 'gear.txt';
|
||||||
|
|
||||||
// 触发下载
|
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
|
|
||||||
// 清理
|
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
showMessage('success', '数据导出成功');
|
showMessage('success', '数据导出成功');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('导出数据时发生错误:', error);
|
console.error('导出数据出错:', error);
|
||||||
showMessage('error', '导出数据失败');
|
showMessage('error', '数据导出失败');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -216,7 +234,6 @@ function CapturePage() {
|
|||||||
const text = e.target?.result as string;
|
const text = e.target?.result as string;
|
||||||
const json = JSON.parse(text);
|
const json = JSON.parse(text);
|
||||||
|
|
||||||
// 校验数据格式
|
|
||||||
if (!json || typeof json !== 'object') {
|
if (!json || typeof json !== 'object') {
|
||||||
showMessage('error', '文件格式错误:不是有效的JSON对象');
|
showMessage('error', '文件格式错误:不是有效的JSON对象');
|
||||||
return;
|
return;
|
||||||
@@ -242,23 +259,20 @@ function CapturePage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 数据校验通过,保存到数据库
|
|
||||||
const sessionName = `import_${Date.now()}`;
|
const sessionName = `import_${Date.now()}`;
|
||||||
const equipmentJSON = JSON.stringify(json.items);
|
const equipmentJSON = JSON.stringify(json.items);
|
||||||
const heroesJSON = JSON.stringify(json.heroes);
|
const heroesJSON = JSON.stringify(json.heroes);
|
||||||
|
|
||||||
await SaveParsedDataToDatabase(sessionName, equipmentJSON, heroesJSON);
|
await SaveParsedDataToDatabase(sessionName, equipmentJSON, heroesJSON);
|
||||||
|
|
||||||
// 更新界面显示
|
|
||||||
const safeData = {
|
const safeData = {
|
||||||
items: json.items,
|
items: json.items,
|
||||||
heroes: json.heroes
|
heroes: json.heroes
|
||||||
};
|
};
|
||||||
setParsedData(safeData);
|
setParsedData(safeData);
|
||||||
setUploadedFileName(''); // 清空文件名显示
|
setUploadedFileName('');
|
||||||
|
|
||||||
showMessage('success', `数据导入成功:${json.items.length}件装备,${json.heroes.length}个英雄,已保存到数据库`);
|
|
||||||
|
|
||||||
|
showMessage('success', `导入成功:${json.items.length}件装备,${json.heroes.length}个英雄`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('文件处理错误:', err);
|
console.error('文件处理错误:', err);
|
||||||
if (err instanceof SyntaxError) {
|
if (err instanceof SyntaxError) {
|
||||||
@@ -276,7 +290,6 @@ function CapturePage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const json = await ReadRawJsonFile();
|
const json = await ReadRawJsonFile();
|
||||||
console.log('已加载本地解析数据:', json);
|
|
||||||
const safeData = {
|
const safeData = {
|
||||||
items: Array.isArray(json.items) ? json.items : [],
|
items: Array.isArray(json.items) ? json.items : [],
|
||||||
heroes: Array.isArray(json.heroes) ? json.heroes : []
|
heroes: Array.isArray(json.heroes) ? json.heroes : []
|
||||||
@@ -306,52 +319,15 @@ function CapturePage() {
|
|||||||
fileInputRef.current?.click();
|
fileInputRef.current?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchInterfaces();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const equipmentColumns = [
|
const equipmentColumns = [
|
||||||
{
|
{ title: 'ID', dataIndex: 'id', key: 'id' },
|
||||||
title: 'ID',
|
{ title: '代码', dataIndex: 'code', key: 'code' },
|
||||||
dataIndex: 'id',
|
{ title: '等级', dataIndex: 'g', key: 'g' },
|
||||||
key: 'id',
|
{ title: '经验', dataIndex: 'e', key: 'e' },
|
||||||
},
|
{ title: '锁定', dataIndex: 'l', key: 'l', render: (locked: boolean) => locked ? '是' : '否' },
|
||||||
{
|
{ title: '魔法值', dataIndex: 'mg', key: 'mg' },
|
||||||
title: '代码',
|
{ title: '力量', dataIndex: 'p', key: 'p' },
|
||||||
dataIndex: 'code',
|
{ title: '技能', dataIndex: 'sk', key: 'sk' },
|
||||||
key: 'code',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '等级',
|
|
||||||
dataIndex: 'g',
|
|
||||||
key: 'g',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '经验',
|
|
||||||
dataIndex: 'e',
|
|
||||||
key: 'e',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '锁定',
|
|
||||||
dataIndex: 'l',
|
|
||||||
key: 'l',
|
|
||||||
render: (locked: boolean) => locked ? '是' : '否',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '魔法值',
|
|
||||||
dataIndex: 'mg',
|
|
||||||
key: 'mg',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '力量',
|
|
||||||
dataIndex: 'p',
|
|
||||||
key: 'p',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '技能',
|
|
||||||
dataIndex: 'sk',
|
|
||||||
key: 'sk',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -359,30 +335,13 @@ function CapturePage() {
|
|||||||
<Sider width={220} style={{ background: '#f5f5f5' }}>
|
<Sider width={220} style={{ background: '#f5f5f5' }}>
|
||||||
<div style={{ padding: '16px 0px 12px 12px' }}>
|
<div style={{ padding: '16px 0px 12px 12px' }}>
|
||||||
<Card title="抓包控制" size="small" style={{ marginBottom: 12, marginTop: 0 }}>
|
<Card title="抓包控制" size="small" style={{ marginBottom: 12, marginTop: 0 }}>
|
||||||
<div style={{ marginBottom: 12 }}>
|
|
||||||
<label>网络接口:</label>
|
|
||||||
<Select
|
|
||||||
style={{ width: '100%', marginTop: 6 }}
|
|
||||||
value={selectedInterface}
|
|
||||||
onChange={setSelectedInterface}
|
|
||||||
placeholder="选择网络接口"
|
|
||||||
loading={interfaceLoading}
|
|
||||||
>
|
|
||||||
{interfaces.map((iface) => (
|
|
||||||
<Select.Option key={iface.name} value={iface.name}>
|
|
||||||
{iface.addresses}
|
|
||||||
</Select.Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlayCircleOutlined />}
|
icon={<PlayCircleOutlined />}
|
||||||
onClick={startCapture}
|
onClick={startCapture}
|
||||||
disabled={isCapturing || !selectedInterface}
|
disabled={isCapturing}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
style={{ flex: 1, height: 32, minWidth: 0, fontSize: 12 }}
|
style={{ flex: 1, height: 32, minWidth: 0, fontSize: 12 }}
|
||||||
>
|
>
|
||||||
@@ -393,7 +352,6 @@ function CapturePage() {
|
|||||||
icon={<StopOutlined />}
|
icon={<StopOutlined />}
|
||||||
onClick={stopCapture}
|
onClick={stopCapture}
|
||||||
disabled={!isCapturing}
|
disabled={!isCapturing}
|
||||||
loading={loading}
|
|
||||||
style={{ flex: 1, height: 32, minWidth: 0, fontSize: 12 }}
|
style={{ flex: 1, height: 32, minWidth: 0, fontSize: 12 }}
|
||||||
>
|
>
|
||||||
停止抓包
|
停止抓包
|
||||||
@@ -429,8 +387,19 @@ function CapturePage() {
|
|||||||
|
|
||||||
<Card title="抓包状态" size="small" style={{ marginTop: 12 }}>
|
<Card title="抓包状态" size="small" style={{ marginTop: 12 }}>
|
||||||
<div>
|
<div>
|
||||||
<p style={{ marginBottom: 4 }}>状态: {isCapturing ? '正在抓包...' : '准备就绪'}</p>
|
<p style={{ marginBottom: 4 }}>
|
||||||
{/*<p style={{ marginBottom: 4 }}>捕获数据: {capturedData.length} 条</p>*/}
|
状态: <span style={{
|
||||||
|
color: (
|
||||||
|
statusText === '未获取到任何数据' ||
|
||||||
|
statusText === '解析失败' ||
|
||||||
|
statusText === '抓包失败'
|
||||||
|
)
|
||||||
|
? '#d32029'
|
||||||
|
: (statusText === '抓包成功,待解析数据' || statusText === '导入成功')
|
||||||
|
? '#389e0d'
|
||||||
|
: undefined
|
||||||
|
}}>{statusText}</span>
|
||||||
|
</p>
|
||||||
<p style={{ marginBottom: 4 }}>英雄数目: {Array.isArray(parsedData?.heroes) ? parsedData.heroes.length : 0} 个</p>
|
<p style={{ marginBottom: 4 }}>英雄数目: {Array.isArray(parsedData?.heroes) ? parsedData.heroes.length : 0} 个</p>
|
||||||
{parsedData && (
|
{parsedData && (
|
||||||
<p style={{ marginBottom: 0 }}>解析装备: {Array.isArray(parsedData?.items) ? parsedData.items.length : 0} 件</p>
|
<p style={{ marginBottom: 0 }}>解析装备: {Array.isArray(parsedData?.items) ? parsedData.items.length : 0} 件</p>
|
||||||
@@ -457,7 +426,7 @@ function CapturePage() {
|
|||||||
<div style={{ textAlign: 'center', padding: '40px' }}>
|
<div style={{ textAlign: 'center', padding: '40px' }}>
|
||||||
<p>暂无数据</p>
|
<p>暂无数据</p>
|
||||||
<p style={{ color: '#999', fontSize: '12px' }}>
|
<p style={{ color: '#999', fontSize: '12px' }}>
|
||||||
{parsedData ? '数据为空,请检查数据源或上传文件' : '请开始抓包或上传JSON文件'}
|
{parsedData ? '数据为空,请检查数据源或导入文件。' : '请开始抓包或导入JSON文件。'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, {useEffect, useState} from 'react';
|
import React, {useEffect, useState} from 'react';
|
||||||
import {Button, Card, Col, Layout, Row, Space, Statistic, Table, Tag} from 'antd';
|
import {Button, Card, Col, Input, Layout, Modal, Row, Select, Space, Statistic, Table, Tag} from 'antd';
|
||||||
import {BarChartOutlined, DatabaseOutlined, ReloadOutlined, SettingOutlined,} from '@ant-design/icons';
|
import {BarChartOutlined, DatabaseOutlined, ReloadOutlined, SettingOutlined,} from '@ant-design/icons';
|
||||||
import * as App from '../../wailsjs/go/service/App';
|
import * as App from '../../wailsjs/go/service/App';
|
||||||
import {model} from '../../wailsjs/go/models';
|
import {model} from '../../wailsjs/go/models';
|
||||||
@@ -25,16 +25,24 @@ interface Equipment {
|
|||||||
const DatabasePage: React.FC = () => {
|
const DatabasePage: React.FC = () => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [latestData, setLatestData] = useState<model.ParsedResult | null>(null);
|
const [latestData, setLatestData] = useState<model.ParsedResult | null>(null);
|
||||||
|
const [sessions, setSessions] = useState<model.ParsedSession[]>([]);
|
||||||
|
const [selectedSessionId, setSelectedSessionId] = useState<number | null>(null);
|
||||||
|
const [renameOpen, setRenameOpen] = useState(false);
|
||||||
|
const [renameValue, setRenameValue] = useState('');
|
||||||
const { success, error, info } = useMessage();
|
const { success, error, info } = useMessage();
|
||||||
|
|
||||||
// 加载最新解析数据
|
const formatSessionLabel = (session: model.ParsedSession) => {
|
||||||
const loadLatestData = async () => {
|
const date = session.created_at ? new Date(session.created_at * 1000) : null;
|
||||||
// 防止重复调用
|
const timeText = date ? date.toLocaleString() : '';
|
||||||
if (loading) return;
|
return timeText ? `${session.session_name}(${timeText})` : session.session_name;
|
||||||
|
};
|
||||||
|
|
||||||
setLoading(true);
|
const loadParsedDataById = async (id: number, skipLoading?: boolean) => {
|
||||||
|
if (!skipLoading) {
|
||||||
|
setLoading(true);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const parsedResult = await App.GetLatestParsedDataFromDatabase();
|
const parsedResult = await App.GetParsedDataByID(id);
|
||||||
if (parsedResult && (parsedResult.items?.length > 0 || parsedResult.heroes?.length > 0)) {
|
if (parsedResult && (parsedResult.items?.length > 0 || parsedResult.heroes?.length > 0)) {
|
||||||
setLatestData(parsedResult);
|
setLatestData(parsedResult);
|
||||||
success('数据加载成功');
|
success('数据加载成功');
|
||||||
@@ -46,13 +54,101 @@ const DatabasePage: React.FC = () => {
|
|||||||
error('加载数据失败');
|
error('加载数据失败');
|
||||||
console.error('Load data error:', err);
|
console.error('Load data error:', err);
|
||||||
setLatestData(null);
|
setLatestData(null);
|
||||||
|
} finally {
|
||||||
|
if (!skipLoading) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadSessions = async (preferLatest?: boolean) => {
|
||||||
|
if (loading) return;
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const list = await App.GetParsedSessions();
|
||||||
|
setSessions(list || []);
|
||||||
|
if (list && list.length > 0) {
|
||||||
|
const nextId = preferLatest ? list[0].id : (selectedSessionId ?? list[0].id);
|
||||||
|
setSelectedSessionId(nextId);
|
||||||
|
await loadParsedDataById(nextId, true);
|
||||||
|
} else {
|
||||||
|
setLatestData(null);
|
||||||
|
info('暂无解析数据');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error('加载数据失败');
|
||||||
|
console.error('Load data error:', err);
|
||||||
|
setLatestData(null);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openRename = () => {
|
||||||
|
if (!selectedSessionId) {
|
||||||
|
info('请先选择一条解析数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const current = sessions.find(s => s.id === selectedSessionId);
|
||||||
|
setRenameValue(current?.session_name || '');
|
||||||
|
setRenameOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRenameOk = async () => {
|
||||||
|
if (!selectedSessionId) {
|
||||||
|
setRenameOpen(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextName = renameValue.trim();
|
||||||
|
if (!nextName) {
|
||||||
|
info('名称不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await App.UpdateParsedSessionName(selectedSessionId, nextName);
|
||||||
|
success('名称已更新');
|
||||||
|
setRenameOpen(false);
|
||||||
|
await loadSessions();
|
||||||
|
} catch (err) {
|
||||||
|
error('更新名称失败');
|
||||||
|
console.error('Rename error:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!selectedSessionId) {
|
||||||
|
info('请先选择一条解析数据');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Modal.confirm({
|
||||||
|
title: '确认删除该解析数据?',
|
||||||
|
content: '删除后无法恢复',
|
||||||
|
okText: '删除',
|
||||||
|
cancelText: '取消',
|
||||||
|
okButtonProps: { danger: true },
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await App.DeleteParsedSession(selectedSessionId);
|
||||||
|
success('已删除');
|
||||||
|
setRenameOpen(false);
|
||||||
|
setSelectedSessionId(null);
|
||||||
|
await loadSessions(true);
|
||||||
|
} catch (err) {
|
||||||
|
error('删除失败');
|
||||||
|
console.error('Delete error:', err);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadLatestData();
|
loadSessions();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 装备表格列定义
|
// 装备表格列定义
|
||||||
@@ -163,19 +259,31 @@ const DatabasePage: React.FC = () => {
|
|||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
onClick={loadLatestData}
|
onClick={loadSessions}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
>
|
>
|
||||||
刷新数据
|
刷新数据
|
||||||
</Button>
|
</Button>
|
||||||
<span style={{ color: '#666' }}>
|
<Select
|
||||||
显示最新一次抓包解析的数据
|
style={{ minWidth: 320 }}
|
||||||
</span>
|
placeholder="请选择解析数据"
|
||||||
|
value={selectedSessionId ?? undefined}
|
||||||
|
onChange={(value: number) => {
|
||||||
|
setSelectedSessionId(value);
|
||||||
|
loadParsedDataById(value);
|
||||||
|
}}
|
||||||
|
options={sessions.map(s => ({
|
||||||
|
value: s.id,
|
||||||
|
label: formatSessionLabel(s),
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<Button onClick={openRename}>重命名</Button>
|
||||||
|
<Button danger onClick={handleDelete}>删除</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 装备表格 */}
|
{/* 装备表格 */}
|
||||||
<Card title="最新解析的装备数据">
|
<Card title="解析的装备数据">
|
||||||
{latestData?.items && latestData.items.length > 0 ? (
|
{latestData?.items && latestData.items.length > 0 ? (
|
||||||
<Table
|
<Table
|
||||||
columns={equipmentColumns}
|
columns={equipmentColumns}
|
||||||
@@ -199,6 +307,21 @@ const DatabasePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
<Modal
|
||||||
|
title="重命名解析数据"
|
||||||
|
open={renameOpen}
|
||||||
|
onOk={handleRenameOk}
|
||||||
|
onCancel={() => setRenameOpen(false)}
|
||||||
|
okText="保存"
|
||||||
|
cancelText="取消"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={renameValue}
|
||||||
|
onChange={e => setRenameValue(e.target.value)}
|
||||||
|
placeholder="请输入名称"
|
||||||
|
maxLength={50}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</Content>
|
</Content>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,6 +48,22 @@ export namespace model {
|
|||||||
this.heroes = source["heroes"];
|
this.heroes = source["heroes"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
export class ParsedSession {
|
||||||
|
id: number;
|
||||||
|
session_name: string;
|
||||||
|
created_at: number;
|
||||||
|
|
||||||
|
static createFrom(source: any = {}) {
|
||||||
|
return new ParsedSession(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(source: any = {}) {
|
||||||
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
|
this.id = source["id"];
|
||||||
|
this.session_name = source["session_name"];
|
||||||
|
this.created_at = source["created_at"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
frontend/wailsjs/go/service/App.d.ts
vendored
10
frontend/wailsjs/go/service/App.d.ts
vendored
@@ -2,6 +2,8 @@
|
|||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
import {model} from '../models';
|
import {model} from '../models';
|
||||||
|
|
||||||
|
export function DeleteParsedSession(arg1:number):Promise<void>;
|
||||||
|
|
||||||
export function ExportCurrentData(arg1:string):Promise<void>;
|
export function ExportCurrentData(arg1:string):Promise<void>;
|
||||||
|
|
||||||
export function ExportData(arg1:Array<string>,arg2:string):Promise<void>;
|
export function ExportData(arg1:Array<string>,arg2:string):Promise<void>;
|
||||||
@@ -20,6 +22,10 @@ export function GetLatestParsedDataFromDatabase():Promise<model.ParsedResult>;
|
|||||||
|
|
||||||
export function GetNetworkInterfaces():Promise<Array<model.NetworkInterface>>;
|
export function GetNetworkInterfaces():Promise<Array<model.NetworkInterface>>;
|
||||||
|
|
||||||
|
export function GetParsedDataByID(arg1:number):Promise<model.ParsedResult>;
|
||||||
|
|
||||||
|
export function GetParsedSessions():Promise<Array<model.ParsedSession>>;
|
||||||
|
|
||||||
export function ParseData(arg1:Array<string>):Promise<string>;
|
export function ParseData(arg1:Array<string>):Promise<string>;
|
||||||
|
|
||||||
export function ReadRawJsonFile():Promise<model.ParsedResult>;
|
export function ReadRawJsonFile():Promise<model.ParsedResult>;
|
||||||
@@ -30,6 +36,10 @@ export function SaveParsedDataToDatabase(arg1:string,arg2:string,arg3:string):Pr
|
|||||||
|
|
||||||
export function StartCapture(arg1:string):Promise<void>;
|
export function StartCapture(arg1:string):Promise<void>;
|
||||||
|
|
||||||
|
export function StartCaptureWithFilter(arg1:string,arg2:string):Promise<void>;
|
||||||
|
|
||||||
export function StopAndParseCapture():Promise<model.ParsedResult>;
|
export function StopAndParseCapture():Promise<model.ParsedResult>;
|
||||||
|
|
||||||
export function StopCapture():Promise<void>;
|
export function StopCapture():Promise<void>;
|
||||||
|
|
||||||
|
export function UpdateParsedSessionName(arg1:number,arg2:string):Promise<void>;
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
// This file is automatically generated. DO NOT EDIT
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
export function DeleteParsedSession(arg1) {
|
||||||
|
return window['go']['service']['App']['DeleteParsedSession'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
export function ExportCurrentData(arg1) {
|
export function ExportCurrentData(arg1) {
|
||||||
return window['go']['service']['App']['ExportCurrentData'](arg1);
|
return window['go']['service']['App']['ExportCurrentData'](arg1);
|
||||||
}
|
}
|
||||||
@@ -38,6 +42,14 @@ export function GetNetworkInterfaces() {
|
|||||||
return window['go']['service']['App']['GetNetworkInterfaces']();
|
return window['go']['service']['App']['GetNetworkInterfaces']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GetParsedDataByID(arg1) {
|
||||||
|
return window['go']['service']['App']['GetParsedDataByID'](arg1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GetParsedSessions() {
|
||||||
|
return window['go']['service']['App']['GetParsedSessions']();
|
||||||
|
}
|
||||||
|
|
||||||
export function ParseData(arg1) {
|
export function ParseData(arg1) {
|
||||||
return window['go']['service']['App']['ParseData'](arg1);
|
return window['go']['service']['App']['ParseData'](arg1);
|
||||||
}
|
}
|
||||||
@@ -58,6 +70,10 @@ export function StartCapture(arg1) {
|
|||||||
return window['go']['service']['App']['StartCapture'](arg1);
|
return window['go']['service']['App']['StartCapture'](arg1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function StartCaptureWithFilter(arg1, arg2) {
|
||||||
|
return window['go']['service']['App']['StartCaptureWithFilter'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|
||||||
export function StopAndParseCapture() {
|
export function StopAndParseCapture() {
|
||||||
return window['go']['service']['App']['StopAndParseCapture']();
|
return window['go']['service']['App']['StopAndParseCapture']();
|
||||||
}
|
}
|
||||||
@@ -65,3 +81,7 @@ export function StopAndParseCapture() {
|
|||||||
export function StopCapture() {
|
export function StopCapture() {
|
||||||
return window['go']['service']['App']['StopCapture']();
|
return window['go']['service']['App']['StopCapture']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function UpdateParsedSessionName(arg1, arg2) {
|
||||||
|
return window['go']['service']['App']['UpdateParsedSessionName'](arg1, arg2);
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ export function EventsOff(eventName, ...additionalEventNames) {
|
|||||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function EventsOffAll() {
|
||||||
|
return window.runtime.EventsOffAll();
|
||||||
|
}
|
||||||
|
|
||||||
export function EventsOnce(eventName, callback) {
|
export function EventsOnce(eventName, callback) {
|
||||||
return EventsOnMultiple(eventName, callback, 1);
|
return EventsOnMultiple(eventName, callback, 1);
|
||||||
}
|
}
|
||||||
|
|||||||
17
go.mod
17
go.mod
@@ -6,17 +6,20 @@ toolchain go1.24.4
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/gopacket v1.1.19
|
github.com/google/gopacket v1.1.19
|
||||||
github.com/mattn/go-sqlite3 v1.14.17
|
github.com/wailsapp/wails/v2 v2.11.0
|
||||||
github.com/wailsapp/wails/v2 v2.10.1
|
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
|
modernc.org/sqlite v1.29.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bep/debounce v1.2.1 // indirect
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||||
github.com/labstack/gommon v0.4.2 // indirect
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
@@ -26,18 +29,26 @@ require (
|
|||||||
github.com/leaanthony/u v1.1.1 // indirect
|
github.com/leaanthony/u v1.1.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/samber/lo v1.49.1 // indirect
|
github.com/samber/lo v1.49.1 // indirect
|
||||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
github.com/wailsapp/go-webview2 v1.0.19 // indirect
|
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/crypto v0.33.0 // indirect
|
golang.org/x/crypto v0.33.0 // indirect
|
||||||
golang.org/x/net v0.35.0 // indirect
|
golang.org/x/net v0.35.0 // indirect
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
|
||||||
|
modernc.org/libc v1.41.0 // indirect
|
||||||
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
|
modernc.org/memory v1.7.2 // indirect
|
||||||
|
modernc.org/strutil v1.2.0 // indirect
|
||||||
|
modernc.org/token v1.1.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
44
go.sum
44
go.sum
@@ -2,14 +2,22 @@ github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
|||||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
|
||||||
|
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||||
@@ -34,14 +42,18 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
|
|||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||||
|
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
@@ -55,12 +67,12 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
|||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
|
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
||||||
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||||
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
|
github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ=
|
||||||
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
|
github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=
|
||||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||||
@@ -73,12 +85,16 @@ golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
|||||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
|
||||||
|
golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -96,8 +112,24 @@ golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
|||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
|
||||||
|
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
|
||||||
|
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
|
||||||
|
modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk=
|
||||||
|
modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY=
|
||||||
|
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
|
||||||
|
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
|
||||||
|
modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
|
||||||
|
modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
|
||||||
|
modernc.org/sqlite v1.29.0 h1:lQVw+ZsFM3aRG5m4myG70tbXpr3S/J1ej0KHIP4EvjM=
|
||||||
|
modernc.org/sqlite v1.29.0/go.mod h1:hG41jCYxOAOoO6BRK66AdRlmOcDzXf7qnwlwjUIOqa0=
|
||||||
|
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
|
||||||
|
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
|
||||||
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
package capture
|
package capture
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"sort"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"equipment-analyzer/internal/model"
|
"equipment-analyzer/internal/model"
|
||||||
@@ -13,13 +17,28 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PacketCapture struct {
|
type PacketCapture struct {
|
||||||
handle *pcap.Handle
|
handles []*pcap.Handle
|
||||||
isCapturing bool
|
isCapturing bool
|
||||||
stopChan chan bool
|
stopChan chan struct{}
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
tcpProcessor *TCPProcessor
|
tcpProcessor *TCPProcessor
|
||||||
dataChan chan *model.TCPData
|
dataChan chan *model.TCPData
|
||||||
errorChan chan error
|
errorChan chan error
|
||||||
|
totalPackets uint64
|
||||||
|
tcpPackets uint64
|
||||||
|
payloads uint64
|
||||||
|
portCounts map[uint16]uint64
|
||||||
|
ipCounts map[string]uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type CaptureStats struct {
|
||||||
|
TotalPackets uint64
|
||||||
|
TCPPackets uint64
|
||||||
|
PayloadPackets uint64
|
||||||
|
AckGroups int
|
||||||
|
UniquePayloads int
|
||||||
|
RawSegments int
|
||||||
|
FinalBuffers int
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -31,10 +50,12 @@ type Config struct {
|
|||||||
|
|
||||||
func NewPacketCapture() *PacketCapture {
|
func NewPacketCapture() *PacketCapture {
|
||||||
return &PacketCapture{
|
return &PacketCapture{
|
||||||
stopChan: make(chan bool),
|
stopChan: make(chan struct{}),
|
||||||
tcpProcessor: NewTCPProcessor(),
|
tcpProcessor: NewTCPProcessor(),
|
||||||
dataChan: make(chan *model.TCPData, 1000),
|
dataChan: make(chan *model.TCPData, 1000),
|
||||||
errorChan: make(chan error, 100),
|
errorChan: make(chan error, 100),
|
||||||
|
portCounts: make(map[uint16]uint64),
|
||||||
|
ipCounts: make(map[string]uint64),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,46 +67,93 @@ func (pc *PacketCapture) Start(config Config) error {
|
|||||||
return fmt.Errorf("capture already running")
|
return fmt.Errorf("capture already running")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 打开网络接口
|
atomic.StoreUint64(&pc.totalPackets, 0)
|
||||||
handle, err := pcap.OpenLive(
|
atomic.StoreUint64(&pc.tcpPackets, 0)
|
||||||
config.InterfaceName,
|
atomic.StoreUint64(&pc.payloads, 0)
|
||||||
int32(config.BufferSize),
|
pc.resetStats()
|
||||||
true, // promiscuous
|
|
||||||
config.Timeout,
|
if pc.stopChan == nil {
|
||||||
)
|
pc.stopChan = make(chan struct{})
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to open interface: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 设置过滤器
|
interfaces := []string{}
|
||||||
if err := handle.SetBPFFilter(config.Filter); err != nil {
|
if config.InterfaceName != "" {
|
||||||
handle.Close()
|
interfaces = []string{config.InterfaceName}
|
||||||
return fmt.Errorf("failed to set filter: %v", err)
|
} else {
|
||||||
|
interfaces = findWorkingInterfaces()
|
||||||
}
|
}
|
||||||
|
|
||||||
pc.handle = handle
|
log.Printf("[capture] start: interfaces=%v filter=%q timeout=%s buffer_size=%d",
|
||||||
|
interfaces, config.Filter, config.Timeout, config.BufferSize)
|
||||||
|
|
||||||
|
var opened []*pcap.Handle
|
||||||
|
for _, iface := range interfaces {
|
||||||
|
handle, err := pcap.OpenLive(
|
||||||
|
iface,
|
||||||
|
int32(config.BufferSize),
|
||||||
|
true,
|
||||||
|
config.Timeout,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[capture] OpenLive failed: interface=%s err=%v", iface, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Filter != "" {
|
||||||
|
if err := handle.SetBPFFilter(config.Filter); err != nil {
|
||||||
|
handle.Close()
|
||||||
|
log.Printf("[capture] SetBPFFilter failed: interface=%s filter=%q err=%v", iface, config.Filter, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("[capture] SetBPFFilter applied: interface=%s filter=%q", iface, config.Filter)
|
||||||
|
} else {
|
||||||
|
log.Printf("[capture] SetBPFFilter skipped: interface=%s filter=empty", iface)
|
||||||
|
}
|
||||||
|
opened = append(opened, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opened) == 0 {
|
||||||
|
return fmt.Errorf("failed to open any interfaces")
|
||||||
|
}
|
||||||
|
|
||||||
|
pc.handles = opened
|
||||||
pc.isCapturing = true
|
pc.isCapturing = true
|
||||||
|
for _, h := range pc.handles {
|
||||||
// 启动抓包协程
|
go pc.captureLoop(h)
|
||||||
go pc.captureLoop()
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pc *PacketCapture) Stop() {
|
func (pc *PacketCapture) Stop() {
|
||||||
pc.mutex.Lock()
|
pc.mutex.Lock()
|
||||||
defer pc.mutex.Unlock()
|
|
||||||
|
|
||||||
if !pc.isCapturing {
|
if !pc.isCapturing {
|
||||||
|
pc.mutex.Unlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pc.isCapturing = false
|
pc.isCapturing = false
|
||||||
close(pc.stopChan)
|
if pc.stopChan != nil {
|
||||||
|
close(pc.stopChan)
|
||||||
if pc.handle != nil {
|
pc.stopChan = nil
|
||||||
pc.handle.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stats := pc.GetStats()
|
||||||
|
portsSnapshot, ipsSnapshot := pc.snapshotStatsLocked()
|
||||||
|
|
||||||
|
for _, h := range pc.handles {
|
||||||
|
if h != nil {
|
||||||
|
h.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pc.handles = nil
|
||||||
|
|
||||||
|
pc.mutex.Unlock()
|
||||||
|
|
||||||
|
log.Printf("[capture] stop: total=%d tcp=%d payload=%d ack_groups=%d unique_payloads=%d raw_segments=%d final_buffers=%d",
|
||||||
|
stats.TotalPackets, stats.TCPPackets, stats.PayloadPackets, stats.AckGroups, stats.UniquePayloads, stats.RawSegments, stats.FinalBuffers)
|
||||||
|
pc.logTopStatsFromSnapshots(portsSnapshot, ipsSnapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pc *PacketCapture) IsCapturing() bool {
|
func (pc *PacketCapture) IsCapturing() bool {
|
||||||
@@ -94,10 +162,9 @@ func (pc *PacketCapture) IsCapturing() bool {
|
|||||||
return pc.isCapturing
|
return pc.isCapturing
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pc *PacketCapture) captureLoop() {
|
func (pc *PacketCapture) captureLoop(handle *pcap.Handle) {
|
||||||
packetSource := gopacket.NewPacketSource(pc.handle, pc.handle.LinkType())
|
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
|
||||||
|
log.Println("[capture] packet loop started")
|
||||||
log.Println("[抓包] 开始监听数据包...")
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -106,37 +173,73 @@ func (pc *PacketCapture) captureLoop() {
|
|||||||
default:
|
default:
|
||||||
packet, err := packetSource.NextPacket()
|
packet, err := packetSource.NextPacket()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, io.EOF) || err.Error() == "EOF" {
|
||||||
|
return
|
||||||
|
}
|
||||||
if err.Error() == "Timeout Expired" {
|
if err.Error() == "Timeout Expired" {
|
||||||
// 静默跳过超时
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
log.Printf("Error reading packet: %v", err)
|
log.Printf("[capture] NextPacket error: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理TCP包
|
atomic.AddUint64(&pc.totalPackets, 1)
|
||||||
pc.processTCPPacket(packet)
|
pc.processTCPPacket(packet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findWorkingInterfaces() []string {
|
||||||
|
devs, err := pcap.FindAllDevs()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[capture] FindAllDevs failed: %v", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dev := range devs {
|
||||||
|
log.Printf("[capture] iface: name=%s desc=%q addrs=%v", dev.Name, dev.Description, dev.Addresses)
|
||||||
|
}
|
||||||
|
|
||||||
|
all := make([]string, 0, len(devs))
|
||||||
|
for _, dev := range devs {
|
||||||
|
all = append(all, dev.Name)
|
||||||
|
}
|
||||||
|
return uniqueStrings(all)
|
||||||
|
}
|
||||||
|
|
||||||
|
func uniqueStrings(in []string) []string {
|
||||||
|
seen := make(map[string]struct{}, len(in))
|
||||||
|
out := make([]string, 0, len(in))
|
||||||
|
for _, v := range in {
|
||||||
|
if _, ok := seen[v]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[v] = struct{}{}
|
||||||
|
out = append(out, v)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func (pc *PacketCapture) processTCPPacket(packet gopacket.Packet) {
|
func (pc *PacketCapture) processTCPPacket(packet gopacket.Packet) {
|
||||||
|
ipLayer := packet.Layer(layers.LayerTypeIPv4)
|
||||||
|
|
||||||
tcpLayer := packet.Layer(layers.LayerTypeTCP)
|
tcpLayer := packet.Layer(layers.LayerTypeTCP)
|
||||||
if tcpLayer == nil {
|
if tcpLayer == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
atomic.AddUint64(&pc.tcpPackets, 1)
|
||||||
|
|
||||||
tcp, ok := tcpLayer.(*layers.TCP)
|
tcp, ok := tcpLayer.(*layers.TCP)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 提取TCP负载
|
|
||||||
if len(tcp.Payload) == 0 {
|
if len(tcp.Payload) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
atomic.AddUint64(&pc.payloads, 1)
|
||||||
|
pc.recordTCPStats(tcp, ipLayer)
|
||||||
|
|
||||||
// 创建TCP数据包
|
|
||||||
tcpData := &model.TCPData{
|
tcpData := &model.TCPData{
|
||||||
Payload: tcp.Payload,
|
Payload: tcp.Payload,
|
||||||
Seq: uint32(tcp.Seq),
|
Seq: uint32(tcp.Seq),
|
||||||
@@ -145,18 +248,101 @@ func (pc *PacketCapture) processTCPPacket(packet gopacket.Packet) {
|
|||||||
DstPort: uint16(tcp.DstPort),
|
DstPort: uint16(tcp.DstPort),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送给TCP处理器
|
|
||||||
pc.tcpProcessor.ProcessPacket(tcpData)
|
pc.tcpProcessor.ProcessPacket(tcpData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pc *PacketCapture) resetStats() {
|
||||||
|
pc.portCounts = make(map[uint16]uint64)
|
||||||
|
pc.ipCounts = make(map[string]uint64)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PacketCapture) recordTCPStats(tcp *layers.TCP, ipLayer gopacket.Layer) {
|
||||||
|
pc.mutex.Lock()
|
||||||
|
defer pc.mutex.Unlock()
|
||||||
|
|
||||||
|
pc.portCounts[uint16(tcp.SrcPort)]++
|
||||||
|
pc.portCounts[uint16(tcp.DstPort)]++
|
||||||
|
|
||||||
|
if ipLayer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ip, ok := ipLayer.(*layers.IPv4); ok {
|
||||||
|
pc.ipCounts[ip.SrcIP.String()]++
|
||||||
|
pc.ipCounts[ip.DstIP.String()]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type kvU16 struct {
|
||||||
|
Key uint16
|
||||||
|
Value uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type kvStr struct {
|
||||||
|
Key string
|
||||||
|
Value uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PacketCapture) snapshotStatsLocked() ([]kvU16, []kvStr) {
|
||||||
|
ports := make([]kvU16, 0, len(pc.portCounts))
|
||||||
|
for k, v := range pc.portCounts {
|
||||||
|
ports = append(ports, kvU16{Key: k, Value: v})
|
||||||
|
}
|
||||||
|
|
||||||
|
ips := make([]kvStr, 0, len(pc.ipCounts))
|
||||||
|
for k, v := range pc.ipCounts {
|
||||||
|
ips = append(ips, kvStr{Key: k, Value: v})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ports, ips
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc *PacketCapture) logTopStatsFromSnapshots(ports []kvU16, ips []kvStr) {
|
||||||
|
sort.Slice(ports, func(i, j int) bool { return ports[i].Value > ports[j].Value })
|
||||||
|
sort.Slice(ips, func(i, j int) bool { return ips[i].Value > ips[j].Value })
|
||||||
|
|
||||||
|
maxPorts := 10
|
||||||
|
if len(ports) < maxPorts {
|
||||||
|
maxPorts = len(ports)
|
||||||
|
}
|
||||||
|
maxIPs := 10
|
||||||
|
if len(ips) < maxIPs {
|
||||||
|
maxIPs = len(ips)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("[capture] top tcp ports: %v", ports[:maxPorts])
|
||||||
|
log.Printf("[capture] top tcp ips: %v", ips[:maxIPs])
|
||||||
|
}
|
||||||
|
|
||||||
func (pc *PacketCapture) GetCapturedData() []string {
|
func (pc *PacketCapture) GetCapturedData() []string {
|
||||||
return pc.tcpProcessor.GetFinalBuffer()
|
return pc.tcpProcessor.GetFinalBuffer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pc *PacketCapture) GetPortCount(port uint16) uint64 {
|
||||||
|
pc.mutex.RLock()
|
||||||
|
defer pc.mutex.RUnlock()
|
||||||
|
return pc.portCounts[port]
|
||||||
|
}
|
||||||
|
|
||||||
func (pc *PacketCapture) ProcessAllData() {
|
func (pc *PacketCapture) ProcessAllData() {
|
||||||
|
start := time.Now()
|
||||||
|
log.Printf("[capture] ProcessAllData started")
|
||||||
pc.tcpProcessor.ProcessAllData()
|
pc.tcpProcessor.ProcessAllData()
|
||||||
|
log.Printf("[capture] ProcessAllData finished in %s", time.Since(start))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pc *PacketCapture) Clear() {
|
func (pc *PacketCapture) Clear() {
|
||||||
pc.tcpProcessor.Clear()
|
pc.tcpProcessor.Clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (pc *PacketCapture) GetStats() CaptureStats {
|
||||||
|
processorStats := pc.tcpProcessor.Stats()
|
||||||
|
return CaptureStats{
|
||||||
|
TotalPackets: atomic.LoadUint64(&pc.totalPackets),
|
||||||
|
TCPPackets: atomic.LoadUint64(&pc.tcpPackets),
|
||||||
|
PayloadPackets: atomic.LoadUint64(&pc.payloads),
|
||||||
|
AckGroups: processorStats.AckGroups,
|
||||||
|
UniquePayloads: processorStats.UniquePayloads,
|
||||||
|
RawSegments: processorStats.RawSegmentCount,
|
||||||
|
FinalBuffers: processorStats.FinalBufferSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,13 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ProcessorStats struct {
|
||||||
|
AckGroups int
|
||||||
|
UniquePayloads int
|
||||||
|
RawSegmentCount int
|
||||||
|
FinalBufferSize int
|
||||||
|
}
|
||||||
|
|
||||||
type TCPProcessor struct {
|
type TCPProcessor struct {
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
ackData map[uint32][]*model.TCPData
|
ackData map[uint32][]*model.TCPData
|
||||||
@@ -94,3 +101,20 @@ func (tp *TCPProcessor) Clear() {
|
|||||||
tp.finalBuffer = make([]string, 0)
|
tp.finalBuffer = make([]string, 0)
|
||||||
tp.loads = make(map[string]bool)
|
tp.loads = make(map[string]bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (tp *TCPProcessor) Stats() ProcessorStats {
|
||||||
|
tp.mutex.RLock()
|
||||||
|
defer tp.mutex.RUnlock()
|
||||||
|
|
||||||
|
rawSegmentCount := 0
|
||||||
|
for _, dataList := range tp.ackData {
|
||||||
|
rawSegmentCount += len(dataList)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProcessorStats{
|
||||||
|
AckGroups: len(tp.ackData),
|
||||||
|
UniquePayloads: len(tp.loads),
|
||||||
|
RawSegmentCount: rawSegmentCount,
|
||||||
|
FinalBufferSize: len(tp.finalBuffer),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ package model
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Database 数据库管理器
|
// Database 数据库管理器
|
||||||
@@ -18,21 +19,25 @@ type Database struct {
|
|||||||
// NewDatabase 创建新的数据库连接
|
// NewDatabase 创建新的数据库连接
|
||||||
func NewDatabase() (*Database, error) {
|
func NewDatabase() (*Database, error) {
|
||||||
dbPath := getDatabasePath()
|
dbPath := getDatabasePath()
|
||||||
|
log.Printf("[db] init: path=%s", dbPath)
|
||||||
|
|
||||||
// 确保目录存在
|
// 确保目录存在
|
||||||
dir := filepath.Dir(dbPath)
|
dir := filepath.Dir(dbPath)
|
||||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||||
|
log.Printf("[db] mkdir failed: dir=%s err=%v", dir, err)
|
||||||
return nil, fmt.Errorf("创建数据库目录失败: %w", err)
|
return nil, fmt.Errorf("创建数据库目录失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接数据库
|
// 连接数据库
|
||||||
db, err := sql.Open("sqlite3", dbPath)
|
db, err := sql.Open("sqlite", dbPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("[db] open failed: path=%s err=%v", dbPath, err)
|
||||||
return nil, fmt.Errorf("连接数据库失败: %w", err)
|
return nil, fmt.Errorf("连接数据库失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试连接
|
// 测试连接
|
||||||
if err := db.Ping(); err != nil {
|
if err := db.Ping(); err != nil {
|
||||||
|
log.Printf("[db] ping failed: err=%v", err)
|
||||||
return nil, fmt.Errorf("数据库连接测试失败: %w", err)
|
return nil, fmt.Errorf("数据库连接测试失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,9 +45,11 @@ func NewDatabase() (*Database, error) {
|
|||||||
|
|
||||||
// 初始化表结构
|
// 初始化表结构
|
||||||
if err := database.initTables(); err != nil {
|
if err := database.initTables(); err != nil {
|
||||||
|
log.Printf("[db] init tables failed: err=%v", err)
|
||||||
return nil, fmt.Errorf("初始化数据库表失败: %w", err)
|
return nil, fmt.Errorf("初始化数据库表失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("[db] init ok")
|
||||||
return database, nil
|
return database, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,13 +122,76 @@ func (d *Database) GetLatestParsedData() (string, string, error) {
|
|||||||
var itemsJSON, heroesJSON string
|
var itemsJSON, heroesJSON string
|
||||||
err := d.db.QueryRow(stmt).Scan(&itemsJSON, &heroesJSON)
|
err := d.db.QueryRow(stmt).Scan(&itemsJSON, &heroesJSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return "", "", nil
|
||||||
|
}
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return itemsJSON, heroesJSON, nil
|
return itemsJSON, heroesJSON, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetParsedSessions 获取所有解析会话
|
||||||
|
func (d *Database) GetParsedSessions() ([]ParsedSession, error) {
|
||||||
|
stmt := `
|
||||||
|
SELECT id, session_name, created_at
|
||||||
|
FROM parsed_data
|
||||||
|
ORDER BY created_at DESC`
|
||||||
|
|
||||||
|
rows, err := d.db.Query(stmt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
sessions := make([]ParsedSession, 0)
|
||||||
|
for rows.Next() {
|
||||||
|
var s ParsedSession
|
||||||
|
if err := rows.Scan(&s.ID, &s.SessionName, &s.CreatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sessions = append(sessions, s)
|
||||||
|
}
|
||||||
|
return sessions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParsedDataByID 获取指定会话的数据
|
||||||
|
func (d *Database) GetParsedDataByID(id int64) (string, string, error) {
|
||||||
|
stmt := `
|
||||||
|
SELECT items_json, heroes_json
|
||||||
|
FROM parsed_data
|
||||||
|
WHERE id = ?
|
||||||
|
LIMIT 1`
|
||||||
|
|
||||||
|
var itemsJSON, heroesJSON string
|
||||||
|
err := d.db.QueryRow(stmt, id).Scan(&itemsJSON, &heroesJSON)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return "", "", nil
|
||||||
|
}
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
return itemsJSON, heroesJSON, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateParsedSessionName 更新解析会话名称
|
||||||
|
func (d *Database) UpdateParsedSessionName(id int64, name string) error {
|
||||||
|
stmt := `
|
||||||
|
UPDATE parsed_data
|
||||||
|
SET session_name = ?
|
||||||
|
WHERE id = ?`
|
||||||
|
_, err := d.db.Exec(stmt, name, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteParsedSession 删除解析会话
|
||||||
|
func (d *Database) DeleteParsedSession(id int64) error {
|
||||||
|
stmt := `
|
||||||
|
DELETE FROM parsed_data
|
||||||
|
WHERE id = ?`
|
||||||
|
_, err := d.db.Exec(stmt, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// SaveSetting 保存应用设置
|
// SaveSetting 保存应用设置
|
||||||
func (d *Database) SaveSetting(key, value string) error {
|
func (d *Database) SaveSetting(key, value string) error {
|
||||||
|
|||||||
@@ -35,3 +35,10 @@ type ParsedResult struct {
|
|||||||
Items []interface{} `json:"items"`
|
Items []interface{} `json:"items"`
|
||||||
Heroes []interface{} `json:"heroes"`
|
Heroes []interface{} `json:"heroes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParsedSession 解析数据会话信息
|
||||||
|
type ParsedSession struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
SessionName string `json:"session_name"`
|
||||||
|
CreatedAt int64 `json:"created_at"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,54 +18,110 @@ type CaptureService struct {
|
|||||||
logger *utils.Logger
|
logger *utils.Logger
|
||||||
packetCapture *capture.PacketCapture
|
packetCapture *capture.PacketCapture
|
||||||
processor *capture.TCPProcessor
|
processor *capture.TCPProcessor
|
||||||
|
parser *ParserService
|
||||||
|
beforeRemote func()
|
||||||
mutex sync.RWMutex
|
mutex sync.RWMutex
|
||||||
isCapturing bool
|
isCapturing bool
|
||||||
|
isStarting bool
|
||||||
|
pendingStop bool
|
||||||
dataChan chan *model.CaptureResult
|
dataChan chan *model.CaptureResult
|
||||||
errorChan chan error
|
errorChan chan error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCaptureService(cfg *config.Config, logger *utils.Logger) *CaptureService {
|
func NewCaptureService(cfg *config.Config, logger *utils.Logger, parser *ParserService) *CaptureService {
|
||||||
return &CaptureService{
|
return &CaptureService{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
packetCapture: capture.NewPacketCapture(),
|
packetCapture: capture.NewPacketCapture(),
|
||||||
processor: capture.NewTCPProcessor(),
|
processor: capture.NewTCPProcessor(),
|
||||||
|
parser: parser,
|
||||||
dataChan: make(chan *model.CaptureResult, 100),
|
dataChan: make(chan *model.CaptureResult, 100),
|
||||||
errorChan: make(chan error, 100),
|
errorChan: make(chan error, 100),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartCapture 开始抓包
|
func (cs *CaptureService) SetBeforeRemote(fn func()) {
|
||||||
func (cs *CaptureService) StartCapture(ctx context.Context, config capture.Config) error {
|
cs.mutex.Lock()
|
||||||
|
defer cs.mutex.Unlock()
|
||||||
|
cs.beforeRemote = fn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cs *CaptureService) StartCaptureAsync(ctx context.Context, config capture.Config, onStarted func(), onError func(error)) error {
|
||||||
cs.mutex.Lock()
|
cs.mutex.Lock()
|
||||||
defer cs.mutex.Unlock()
|
defer cs.mutex.Unlock()
|
||||||
|
|
||||||
if cs.isCapturing {
|
if cs.isCapturing || cs.isStarting {
|
||||||
return fmt.Errorf("capture already running")
|
return fmt.Errorf("capture already running")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cs.packetCapture.Start(config); err != nil {
|
cs.isStarting = true
|
||||||
return fmt.Errorf("failed to start capture: %w", err)
|
cs.pendingStop = false
|
||||||
}
|
|
||||||
|
|
||||||
cs.isCapturing = true
|
cs.logger.Info("StartCapture requested",
|
||||||
cs.logger.Info("Packet capture started", "interface", config.InterfaceName)
|
"interface", config.InterfaceName,
|
||||||
|
"filter", config.Filter,
|
||||||
|
"timeout_ms", config.Timeout.Milliseconds(),
|
||||||
|
"buffer_size", config.BufferSize,
|
||||||
|
)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := cs.packetCapture.Start(config); err != nil {
|
||||||
|
cs.logger.Error("Packet capture start failed", "error", err)
|
||||||
|
cs.mutex.Lock()
|
||||||
|
cs.isStarting = false
|
||||||
|
cs.mutex.Unlock()
|
||||||
|
if onError != nil {
|
||||||
|
onError(fmt.Errorf("failed to start capture: %w", err))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldStop bool
|
||||||
|
cs.mutex.Lock()
|
||||||
|
cs.isStarting = false
|
||||||
|
cs.isCapturing = true
|
||||||
|
shouldStop = cs.pendingStop
|
||||||
|
cs.pendingStop = false
|
||||||
|
cs.mutex.Unlock()
|
||||||
|
|
||||||
|
cs.logger.Info("Packet capture started", "interface", config.InterfaceName)
|
||||||
|
if onStarted != nil {
|
||||||
|
onStarted()
|
||||||
|
}
|
||||||
|
if shouldStop {
|
||||||
|
_, _ = cs.StopAndParseCapture()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// 启动数据处理协程
|
|
||||||
go cs.processData(ctx)
|
go cs.processData(ctx)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopCapture 停止抓包
|
|
||||||
func (cs *CaptureService) StopCapture() error {
|
func (cs *CaptureService) StopCapture() error {
|
||||||
cs.mutex.Lock()
|
cs.mutex.Lock()
|
||||||
defer cs.mutex.Unlock()
|
defer cs.mutex.Unlock()
|
||||||
|
|
||||||
|
if cs.isStarting {
|
||||||
|
cs.pendingStop = true
|
||||||
|
cs.logger.Info("StopCapture queued while starting")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if !cs.isCapturing {
|
if !cs.isCapturing {
|
||||||
return fmt.Errorf("capture not running")
|
return fmt.Errorf("capture not running")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeStats := cs.packetCapture.GetStats()
|
||||||
|
cs.logger.Info("StopCapture requested",
|
||||||
|
"total_packets", beforeStats.TotalPackets,
|
||||||
|
"tcp_packets", beforeStats.TCPPackets,
|
||||||
|
"payload_packets", beforeStats.PayloadPackets,
|
||||||
|
"ack_groups", beforeStats.AckGroups,
|
||||||
|
"unique_payloads", beforeStats.UniquePayloads,
|
||||||
|
"raw_segments", beforeStats.RawSegments,
|
||||||
|
"final_buffers", beforeStats.FinalBuffers,
|
||||||
|
)
|
||||||
|
|
||||||
cs.packetCapture.Stop()
|
cs.packetCapture.Stop()
|
||||||
cs.isCapturing = false
|
cs.isCapturing = false
|
||||||
cs.logger.Info("Packet capture stopped")
|
cs.logger.Info("Packet capture stopped")
|
||||||
@@ -72,62 +129,139 @@ func (cs *CaptureService) StopCapture() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCapturedData 获取抓包数据
|
|
||||||
func (cs *CaptureService) GetCapturedData() []string {
|
func (cs *CaptureService) GetCapturedData() []string {
|
||||||
return cs.packetCapture.GetCapturedData()
|
return cs.packetCapture.GetCapturedData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProcessAllData 处理所有数据
|
|
||||||
func (cs *CaptureService) ProcessAllData() {
|
func (cs *CaptureService) ProcessAllData() {
|
||||||
cs.packetCapture.ProcessAllData()
|
cs.packetCapture.ProcessAllData()
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsCapturing 检查是否正在抓包
|
|
||||||
func (cs *CaptureService) IsCapturing() bool {
|
func (cs *CaptureService) IsCapturing() bool {
|
||||||
cs.mutex.RLock()
|
cs.mutex.RLock()
|
||||||
defer cs.mutex.RUnlock()
|
defer cs.mutex.RUnlock()
|
||||||
return cs.isCapturing
|
return cs.isCapturing
|
||||||
}
|
}
|
||||||
|
|
||||||
// processData 处理抓包数据
|
func (cs *CaptureService) IsStarting() bool {
|
||||||
|
cs.mutex.RLock()
|
||||||
|
defer cs.mutex.RUnlock()
|
||||||
|
return cs.isStarting
|
||||||
|
}
|
||||||
|
|
||||||
func (cs *CaptureService) processData(ctx context.Context) {
|
func (cs *CaptureService) processData(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(3 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
default:
|
case <-ticker.C:
|
||||||
// 这里可以添加实时数据处理逻辑
|
if !cs.IsCapturing() {
|
||||||
time.Sleep(100 * time.Millisecond)
|
continue
|
||||||
|
}
|
||||||
|
stats := cs.packetCapture.GetStats()
|
||||||
|
cs.logger.Info("Capture heartbeat",
|
||||||
|
"total_packets", stats.TotalPackets,
|
||||||
|
"tcp_packets", stats.TCPPackets,
|
||||||
|
"payload_packets", stats.PayloadPackets,
|
||||||
|
"ack_groups", stats.AckGroups,
|
||||||
|
"unique_payloads", stats.UniquePayloads,
|
||||||
|
"raw_segments", stats.RawSegments,
|
||||||
|
"final_buffers", stats.FinalBuffers,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopAndParseCapture 停止抓包并解析数据
|
func (cs *CaptureService) StopAndParseCapture() (*model.ParsedResult, error) {
|
||||||
func (cs *CaptureService) StopAndParseCapture(parser *ParserService) (*model.ParsedResult, error) {
|
log.Printf("[service] StopAndParseCapture enter")
|
||||||
cs.mutex.Lock()
|
cs.mutex.Lock()
|
||||||
defer cs.mutex.Unlock()
|
if cs.isStarting {
|
||||||
|
cs.pendingStop = true
|
||||||
|
cs.mutex.Unlock()
|
||||||
|
log.Printf("[service] StopAndParseCapture queued while starting")
|
||||||
|
return nil, fmt.Errorf("capture starting")
|
||||||
|
}
|
||||||
if !cs.isCapturing {
|
if !cs.isCapturing {
|
||||||
|
cs.mutex.Unlock()
|
||||||
|
log.Printf("[service] StopAndParseCapture exit: not running")
|
||||||
return nil, fmt.Errorf("capture not running")
|
return nil, fmt.Errorf("capture not running")
|
||||||
}
|
}
|
||||||
|
cs.mutex.Unlock()
|
||||||
|
|
||||||
|
beforeStop := cs.packetCapture.GetStats()
|
||||||
|
cs.logger.Info("StopAndParseCapture requested",
|
||||||
|
"total_packets", beforeStop.TotalPackets,
|
||||||
|
"tcp_packets", beforeStop.TCPPackets,
|
||||||
|
"payload_packets", beforeStop.PayloadPackets,
|
||||||
|
"ack_groups", beforeStop.AckGroups,
|
||||||
|
"unique_payloads", beforeStop.UniquePayloads,
|
||||||
|
"raw_segments", beforeStop.RawSegments,
|
||||||
|
"final_buffers", beforeStop.FinalBuffers,
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Printf("[service] StopAndParseCapture stopping packet capture")
|
||||||
cs.packetCapture.Stop()
|
cs.packetCapture.Stop()
|
||||||
|
log.Printf("[service] StopAndParseCapture packet capture stopped")
|
||||||
cs.isCapturing = false
|
cs.isCapturing = false
|
||||||
cs.logger.Info("Packet capture stopped (StopAndParseCapture)")
|
cs.logger.Info("Packet capture stopped (StopAndParseCapture)")
|
||||||
|
|
||||||
// 处理所有收集的数据
|
|
||||||
cs.packetCapture.ProcessAllData()
|
cs.packetCapture.ProcessAllData()
|
||||||
|
afterProcess := cs.packetCapture.GetStats()
|
||||||
|
cs.logger.Info("ProcessAllData finished",
|
||||||
|
"total_packets", afterProcess.TotalPackets,
|
||||||
|
"tcp_packets", afterProcess.TCPPackets,
|
||||||
|
"payload_packets", afterProcess.PayloadPackets,
|
||||||
|
"ack_groups", afterProcess.AckGroups,
|
||||||
|
"unique_payloads", afterProcess.UniquePayloads,
|
||||||
|
"raw_segments", afterProcess.RawSegments,
|
||||||
|
"final_buffers", afterProcess.FinalBuffers,
|
||||||
|
)
|
||||||
|
|
||||||
// 获取抓包数据
|
port5222 := cs.packetCapture.GetPortCount(5222)
|
||||||
rawData := cs.packetCapture.GetCapturedData()
|
port3333 := cs.packetCapture.GetPortCount(3333)
|
||||||
if len(rawData) == 0 {
|
if port5222 == 0 && port3333 == 0 {
|
||||||
|
cs.logger.Warn("No target port data after processing",
|
||||||
|
"port_5222", port5222,
|
||||||
|
"port_3333", port3333,
|
||||||
|
)
|
||||||
return nil, fmt.Errorf("no captured data")
|
return nil, fmt.Errorf("no captured data")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析数据
|
if afterProcess.FinalBuffers == 0 {
|
||||||
result, _, err := parser.ParseHexData(rawData)
|
cs.logger.Warn("No captured data after processing", "hint", "check interface and bpf filter")
|
||||||
|
return nil, fmt.Errorf("no captured data")
|
||||||
|
}
|
||||||
|
|
||||||
|
rawData := cs.packetCapture.GetCapturedData()
|
||||||
|
cs.logger.Info("Captured raw data snapshot", "hex_chunks", len(rawData))
|
||||||
|
if len(rawData) == 0 {
|
||||||
|
cs.logger.Warn("No captured data after stop", "hint", "check interface and bpf filter")
|
||||||
|
return nil, fmt.Errorf("no captured data")
|
||||||
|
}
|
||||||
|
|
||||||
|
parseStart := time.Now()
|
||||||
|
cs.logger.Info("ParseHexData starting",
|
||||||
|
"port_5222", port5222,
|
||||||
|
"port_3333", port3333,
|
||||||
|
"final_buffers", afterProcess.FinalBuffers,
|
||||||
|
"hex_chunks", len(rawData),
|
||||||
|
)
|
||||||
|
cs.mutex.RLock()
|
||||||
|
beforeRemote := cs.beforeRemote
|
||||||
|
cs.mutex.RUnlock()
|
||||||
|
if beforeRemote != nil {
|
||||||
|
beforeRemote()
|
||||||
|
}
|
||||||
|
result, _, err := cs.parser.ParseHexData(rawData)
|
||||||
|
cs.logger.Info("ParseHexData finished", "duration_ms", time.Since(parseStart).Milliseconds())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
cs.logger.Error("ParseHexData failed", "error", err, "hex_chunks", len(rawData))
|
||||||
return nil, fmt.Errorf("解析数据失败: %v", err)
|
return nil, fmt.Errorf("解析数据失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cs.logger.Info("ParseHexData succeeded", "items", len(result.Items), "heroes", len(result.Heroes))
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,44 @@ func (s *DatabaseService) GetLatestParsedDataFromDatabase() (string, string, err
|
|||||||
return itemsJSON, heroesJSON, nil
|
return itemsJSON, heroesJSON, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetParsedSessions 从数据库获取所有解析会话
|
||||||
|
func (s *DatabaseService) GetParsedSessions() ([]model.ParsedSession, error) {
|
||||||
|
sessions, err := s.db.GetParsedSessions()
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("从数据库获取解析会话失败", "error", err)
|
||||||
|
return nil, fmt.Errorf("获取解析会话失败: %w", err)
|
||||||
|
}
|
||||||
|
return sessions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParsedDataByID 从数据库获取指定会话数据
|
||||||
|
func (s *DatabaseService) GetParsedDataByID(id int64) (string, string, error) {
|
||||||
|
itemsJSON, heroesJSON, err := s.db.GetParsedDataByID(id)
|
||||||
|
if err != nil {
|
||||||
|
s.logger.Error("从数据库获取解析数据失败", "error", err, "id", id)
|
||||||
|
return "", "", fmt.Errorf("获取解析数据失败: %w", err)
|
||||||
|
}
|
||||||
|
return itemsJSON, heroesJSON, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateParsedSessionName 更新解析会话名称
|
||||||
|
func (s *DatabaseService) UpdateParsedSessionName(id int64, name string) error {
|
||||||
|
if err := s.db.UpdateParsedSessionName(id, name); err != nil {
|
||||||
|
s.logger.Error("更新解析会话名称失败", "error", err, "id", id)
|
||||||
|
return fmt.Errorf("更新解析会话名称失败: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteParsedSession 删除解析会话
|
||||||
|
func (s *DatabaseService) DeleteParsedSession(id int64) error {
|
||||||
|
if err := s.db.DeleteParsedSession(id); err != nil {
|
||||||
|
s.logger.Error("删除解析会话失败", "error", err, "id", id)
|
||||||
|
return fmt.Errorf("删除解析会话失败: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// SaveAppSetting 保存应用设置
|
// SaveAppSetting 保存应用设置
|
||||||
func (s *DatabaseService) SaveAppSetting(key, value string) error {
|
func (s *DatabaseService) SaveAppSetting(key, value string) error {
|
||||||
err := s.db.SaveSetting(key, value)
|
err := s.db.SaveSetting(key, value)
|
||||||
@@ -78,5 +116,3 @@ func (s *DatabaseService) GetAllAppSettings() (map[string]string, error) {
|
|||||||
|
|
||||||
return settings, nil
|
return settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -4,15 +4,18 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"equipment-analyzer/internal/capture"
|
"equipment-analyzer/internal/capture"
|
||||||
"equipment-analyzer/internal/config"
|
"equipment-analyzer/internal/config"
|
||||||
"equipment-analyzer/internal/model"
|
"equipment-analyzer/internal/model"
|
||||||
"equipment-analyzer/internal/utils"
|
"equipment-analyzer/internal/utils"
|
||||||
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
)
|
)
|
||||||
|
|
||||||
type App struct {
|
type App struct {
|
||||||
|
ctx context.Context
|
||||||
config *config.Config
|
config *config.Config
|
||||||
logger *utils.Logger
|
logger *utils.Logger
|
||||||
captureService *CaptureService
|
captureService *CaptureService
|
||||||
@@ -22,217 +25,228 @@ type App struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(cfg *config.Config, logger *utils.Logger) *App {
|
func NewApp(cfg *config.Config, logger *utils.Logger) *App {
|
||||||
// 初始化数据库
|
// init database
|
||||||
database, err := model.NewDatabase()
|
database, err := model.NewDatabase()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("初始化数据库失败", "error", err)
|
logger.Error("database init failed", "error", err)
|
||||||
// 如果数据库初始化失败,仍然创建应用,但数据库功能不可用
|
// allow app to run without db features
|
||||||
|
parserService := NewParserService(cfg, logger)
|
||||||
return &App{
|
return &App{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
captureService: NewCaptureService(cfg, logger),
|
captureService: NewCaptureService(cfg, logger, parserService),
|
||||||
parserService: NewParserService(cfg, logger),
|
parserService: parserService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
databaseService := NewDatabaseService(database, logger)
|
databaseService := NewDatabaseService(database, logger)
|
||||||
|
|
||||||
|
parserService := NewParserService(cfg, logger)
|
||||||
return &App{
|
return &App{
|
||||||
config: cfg,
|
config: cfg,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
captureService: NewCaptureService(cfg, logger),
|
captureService: NewCaptureService(cfg, logger, parserService),
|
||||||
parserService: NewParserService(cfg, logger),
|
parserService: parserService,
|
||||||
database: database,
|
database: database,
|
||||||
databaseService: databaseService,
|
databaseService: databaseService,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Startup(ctx context.Context) {
|
func (a *App) Startup(ctx context.Context) {
|
||||||
a.logger.Info("应用启动")
|
a.ctx = ctx
|
||||||
|
a.captureService.SetBeforeRemote(func() {
|
||||||
|
if a.ctx != nil {
|
||||||
|
runtime.EventsEmit(a.ctx, "capture:ready_to_parse")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
a.logger.Info("app startup")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) DomReady(ctx context.Context) {
|
func (a *App) DomReady(ctx context.Context) {
|
||||||
a.logger.Info("DOM准备就绪")
|
a.logger.Info("dom ready")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) BeforeClose(ctx context.Context) (prevent bool) {
|
func (a *App) BeforeClose(ctx context.Context) (prevent bool) {
|
||||||
a.logger.Info("应用即将关闭")
|
a.logger.Info("app closing")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Shutdown(ctx context.Context) {
|
func (a *App) Shutdown(ctx context.Context) {
|
||||||
a.logger.Info("应用关闭")
|
a.logger.Info("app shutdown")
|
||||||
|
|
||||||
// 关闭数据库连接
|
|
||||||
if a.database != nil {
|
if a.database != nil {
|
||||||
if err := a.database.Close(); err != nil {
|
if err := a.database.Close(); err != nil {
|
||||||
a.logger.Error("关闭数据库连接失败", "error", err)
|
a.logger.Error("failed to close database connection", "error", err)
|
||||||
} else {
|
} else {
|
||||||
a.logger.Info("数据库连接已关闭")
|
a.logger.Info("database connection closed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNetworkInterfaces 获取网络接口列表
|
// GetNetworkInterfaces returns available network interfaces.
|
||||||
func (a *App) GetNetworkInterfaces() ([]model.NetworkInterface, error) {
|
func (a *App) GetNetworkInterfaces() ([]model.NetworkInterface, error) {
|
||||||
interfaces, err := capture.GetNetworkInterfaces()
|
interfaces, err := capture.GetNetworkInterfaces()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("获取网络接口失败", "error", err)
|
a.logger.Error("get network interfaces failed", "error", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return interfaces, nil
|
return interfaces, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StartCapture 开始抓包
|
// StartCapture starts capture on the given interface.
|
||||||
func (a *App) StartCapture(interfaceName string) error {
|
func (a *App) StartCapture(interfaceName string) error {
|
||||||
if a.captureService.IsCapturing() {
|
if a.captureService.IsCapturing() {
|
||||||
return fmt.Errorf("抓包已在进行中")
|
return fmt.Errorf("capture already running")
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutMs := a.config.Capture.DefaultTimeout
|
||||||
|
if timeoutMs > 500 {
|
||||||
|
log.Printf("[service] capture timeout too high (%dms), clamp to 500ms for responsive stop", timeoutMs)
|
||||||
|
timeoutMs = 500
|
||||||
}
|
}
|
||||||
|
|
||||||
config := capture.Config{
|
config := capture.Config{
|
||||||
InterfaceName: interfaceName,
|
InterfaceName: interfaceName,
|
||||||
Filter: a.config.Capture.DefaultFilter,
|
Filter: a.config.Capture.DefaultFilter,
|
||||||
Timeout: time.Duration(a.config.Capture.DefaultTimeout) * time.Millisecond,
|
Timeout: time.Duration(timeoutMs) * time.Millisecond,
|
||||||
BufferSize: a.config.Capture.BufferSize,
|
BufferSize: a.config.Capture.BufferSize,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := a.captureService.StartCapture(context.Background(), config)
|
err := a.captureService.StartCaptureAsync(context.Background(), config, func() {
|
||||||
|
if a.ctx != nil {
|
||||||
|
runtime.EventsEmit(a.ctx, "capture:started")
|
||||||
|
}
|
||||||
|
}, func(err error) {
|
||||||
|
a.logger.Error("start capture failed", "error", err)
|
||||||
|
if a.ctx != nil {
|
||||||
|
runtime.EventsEmit(a.ctx, "capture:start_failed", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("开始抓包失败", "error", err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
a.logger.Info("capture start requested", "interface", interfaceName)
|
||||||
a.logger.Info("抓包开始", "interface", interfaceName)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopCapture 停止抓包
|
// StopCapture stops capture.
|
||||||
func (a *App) StopCapture() error {
|
func (a *App) StopCapture() error {
|
||||||
if !a.captureService.IsCapturing() {
|
if !a.captureService.IsCapturing() {
|
||||||
return fmt.Errorf("没有正在进行的抓包")
|
return fmt.Errorf("capture not running")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := a.captureService.StopCapture()
|
err := a.captureService.StopCapture()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("停止抓包失败", "error", err)
|
a.logger.Error("stop capture failed", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理所有收集的数据
|
|
||||||
a.captureService.ProcessAllData()
|
a.captureService.ProcessAllData()
|
||||||
|
|
||||||
a.logger.Info("抓包停止")
|
a.logger.Info("capture stopped")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCapturedData 获取抓包数据
|
// GetCapturedData returns raw captured data.
|
||||||
func (a *App) GetCapturedData() ([]string, error) {
|
func (a *App) GetCapturedData() ([]string, error) {
|
||||||
return a.captureService.GetCapturedData(), nil
|
return a.captureService.GetCapturedData(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseData 解析数据为JSON
|
// ParseData parses captured data to JSON (remote parser).
|
||||||
func (a *App) ParseData(hexDataList []string) (string, error) {
|
func (a *App) ParseData(hexDataList []string) (string, error) {
|
||||||
_, rawJson, err := a.parserService.ParseHexData(hexDataList)
|
_, rawJson, err := a.parserService.ParseHexData(hexDataList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("解析数据失败", "error", err)
|
a.logger.Error("parse data failed", "error", err)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return rawJson, nil
|
return rawJson, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportData 导出数据到文件
|
// ExportData exports data to a file.
|
||||||
func (a *App) ExportData(hexDataList []string, filename string) error {
|
func (a *App) ExportData(hexDataList []string, filename string) error {
|
||||||
result, rawJson, err := a.parserService.ParseHexData(hexDataList)
|
result, rawJson, err := a.parserService.ParseHexData(hexDataList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("解析数据失败", "error", err)
|
a.logger.Error("parse data failed", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// 这里可以添加文件写入逻辑
|
|
||||||
a.logger.Info("导出数据", "filename", filename, "count", len(result.Items))
|
a.logger.Info("export data", "filename", filename, "count", len(result.Items))
|
||||||
// 简单示例:写入到当前目录
|
|
||||||
err = utils.WriteFile(filename, []byte(rawJson))
|
err = utils.WriteFile(filename, []byte(rawJson))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("写入文件失败", "error", err)
|
a.logger.Error("write file failed", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExportCurrentData 导出当前数据库中的数据到文件
|
// ExportCurrentData exports latest data from database to a file.
|
||||||
func (a *App) ExportCurrentData(filename string) error {
|
func (a *App) ExportCurrentData(filename string) error {
|
||||||
if a.databaseService == nil {
|
if a.databaseService == nil {
|
||||||
return fmt.Errorf("数据库服务未初始化")
|
return fmt.Errorf("database service not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从数据库获取最新数据
|
|
||||||
parsedResult, err := a.GetLatestParsedDataFromDatabase()
|
parsedResult, err := a.GetLatestParsedDataFromDatabase()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("获取数据库数据失败", "error", err)
|
a.logger.Error("failed to load data from database", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsedResult == nil || (len(parsedResult.Items) == 0 && len(parsedResult.Heroes) == 0) {
|
if parsedResult == nil || (len(parsedResult.Items) == 0 && len(parsedResult.Heroes) == 0) {
|
||||||
return fmt.Errorf("没有数据可导出")
|
return fmt.Errorf("no data to export")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建导出数据格式
|
|
||||||
exportData := map[string]interface{}{
|
exportData := map[string]interface{}{
|
||||||
"items": parsedResult.Items,
|
"items": parsedResult.Items,
|
||||||
"heroes": parsedResult.Heroes,
|
"heroes": parsedResult.Heroes,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 序列化为JSON
|
|
||||||
jsonData, err := json.MarshalIndent(exportData, "", " ")
|
jsonData, err := json.MarshalIndent(exportData, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("序列化数据失败", "error", err)
|
a.logger.Error("failed to marshal data", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 写入文件
|
|
||||||
err = utils.WriteFile(filename, jsonData)
|
err = utils.WriteFile(filename, jsonData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("写入文件失败", "error", err)
|
a.logger.Error("write file failed", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
a.logger.Info("数据导出成功", "filename", filename, "items_count", len(parsedResult.Items), "heroes_count", len(parsedResult.Heroes))
|
a.logger.Info("export current data succeeded", "filename", filename, "items_count", len(parsedResult.Items), "heroes_count", len(parsedResult.Heroes))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCurrentDataForExport 获取当前数据库中的数据,供前端导出使用
|
// GetCurrentDataForExport returns latest data from database as JSON string.
|
||||||
func (a *App) GetCurrentDataForExport() (string, error) {
|
func (a *App) GetCurrentDataForExport() (string, error) {
|
||||||
if a.databaseService == nil {
|
if a.databaseService == nil {
|
||||||
return "", fmt.Errorf("数据库服务未初始化")
|
return "", fmt.Errorf("database service not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从数据库获取最新数据
|
|
||||||
parsedResult, err := a.GetLatestParsedDataFromDatabase()
|
parsedResult, err := a.GetLatestParsedDataFromDatabase()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("获取数据库数据失败", "error", err)
|
a.logger.Error("failed to load data from database", "error", err)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
if parsedResult == nil || (len(parsedResult.Items) == 0 && len(parsedResult.Heroes) == 0) {
|
if parsedResult == nil || (len(parsedResult.Items) == 0 && len(parsedResult.Heroes) == 0) {
|
||||||
return "", fmt.Errorf("没有数据可导出")
|
return "", fmt.Errorf("no data to export")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建导出数据格式
|
|
||||||
exportData := map[string]interface{}{
|
exportData := map[string]interface{}{
|
||||||
"items": parsedResult.Items,
|
"items": parsedResult.Items,
|
||||||
"heroes": parsedResult.Heroes,
|
"heroes": parsedResult.Heroes,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 序列化为JSON
|
|
||||||
jsonData, err := json.MarshalIndent(exportData, "", " ")
|
jsonData, err := json.MarshalIndent(exportData, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("序列化数据失败", "error", err)
|
a.logger.Error("failed to marshal data", "error", err)
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return string(jsonData), nil
|
return string(jsonData), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCaptureStatus 获取抓包状态
|
// GetCaptureStatus returns capture status.
|
||||||
func (a *App) GetCaptureStatus() model.CaptureStatus {
|
func (a *App) GetCaptureStatus() model.CaptureStatus {
|
||||||
return model.CaptureStatus{
|
return model.CaptureStatus{
|
||||||
IsCapturing: a.captureService.IsCapturing(),
|
IsCapturing: a.captureService.IsCapturing(),
|
||||||
@@ -242,27 +256,29 @@ func (a *App) GetCaptureStatus() model.CaptureStatus {
|
|||||||
|
|
||||||
func (a *App) getStatusMessage() string {
|
func (a *App) getStatusMessage() string {
|
||||||
if a.captureService.IsCapturing() {
|
if a.captureService.IsCapturing() {
|
||||||
return "正在抓包..."
|
return "capturing"
|
||||||
}
|
}
|
||||||
return "准备就绪"
|
if a.captureService.IsStarting() {
|
||||||
|
return "starting"
|
||||||
|
}
|
||||||
|
return "ready"
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadRawJsonFile 已废弃,请使用GetLatestParsedDataFromDatabase从数据库获取数据
|
// ReadRawJsonFile is deprecated; use GetLatestParsedDataFromDatabase.
|
||||||
func (a *App) ReadRawJsonFile() (*model.ParsedResult, error) {
|
func (a *App) ReadRawJsonFile() (*model.ParsedResult, error) {
|
||||||
return a.GetLatestParsedDataFromDatabase()
|
return a.GetLatestParsedDataFromDatabase()
|
||||||
}
|
}
|
||||||
|
|
||||||
// StopAndParseCapture 停止抓包并解析数据,供前端调用
|
// StopAndParseCapture stops capture and parses data.
|
||||||
func (a *App) StopAndParseCapture() (*model.ParsedResult, error) {
|
func (a *App) StopAndParseCapture() (*model.ParsedResult, error) {
|
||||||
result, err := a.captureService.StopAndParseCapture(a.parserService)
|
log.Printf("[service] StopAndParseCapture entry")
|
||||||
|
result, err := a.captureService.StopAndParseCapture()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
a.logger.Error("停止抓包并解析数据失败", "error", err)
|
a.logger.Error("stop and parse capture failed", "error", err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将解析结果保存到数据库
|
|
||||||
if a.databaseService != nil && result != nil {
|
if a.databaseService != nil && result != nil {
|
||||||
// 序列化装备数据
|
|
||||||
itemsJSON := "[]"
|
itemsJSON := "[]"
|
||||||
if result.Items != nil {
|
if result.Items != nil {
|
||||||
if jsonData, err := json.Marshal(result.Items); err == nil {
|
if jsonData, err := json.Marshal(result.Items); err == nil {
|
||||||
@@ -270,7 +286,6 @@ func (a *App) StopAndParseCapture() (*model.ParsedResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 序列化英雄数据
|
|
||||||
heroesJSON := "[]"
|
heroesJSON := "[]"
|
||||||
if result.Heroes != nil {
|
if result.Heroes != nil {
|
||||||
if jsonData, err := json.Marshal(result.Heroes); err == nil {
|
if jsonData, err := json.Marshal(result.Heroes); err == nil {
|
||||||
@@ -278,33 +293,29 @@ func (a *App) StopAndParseCapture() (*model.ParsedResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存到数据库
|
|
||||||
sessionName := fmt.Sprintf("capture_%d", time.Now().Unix())
|
sessionName := fmt.Sprintf("capture_%d", time.Now().Unix())
|
||||||
if err := a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON); err != nil {
|
if err := a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON); err != nil {
|
||||||
a.logger.Error("保存解析数据到数据库失败", "error", err)
|
a.logger.Error("save parsed data failed", "error", err)
|
||||||
// 不返回错误,因为解析成功了,只是保存失败
|
|
||||||
} else {
|
} else {
|
||||||
a.logger.Info("解析数据已保存到数据库", "session_name", sessionName)
|
a.logger.Info("parsed data saved", "session_name", sessionName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== 数据库相关API ==========
|
// SaveParsedDataToDatabase saves parsed data.
|
||||||
|
|
||||||
// SaveParsedDataToDatabase 保存解析后的数据到数据库
|
|
||||||
func (a *App) SaveParsedDataToDatabase(sessionName string, itemsJSON, heroesJSON string) error {
|
func (a *App) SaveParsedDataToDatabase(sessionName string, itemsJSON, heroesJSON string) error {
|
||||||
if a.databaseService == nil {
|
if a.databaseService == nil {
|
||||||
return fmt.Errorf("数据库服务未初始化")
|
return fmt.Errorf("database service not initialized")
|
||||||
}
|
}
|
||||||
return a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON)
|
return a.databaseService.SaveParsedDataToDatabase(sessionName, itemsJSON, heroesJSON)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLatestParsedDataFromDatabase 从数据库获取最新的解析数据
|
// GetLatestParsedDataFromDatabase returns latest parsed data from database.
|
||||||
func (a *App) GetLatestParsedDataFromDatabase() (*model.ParsedResult, error) {
|
func (a *App) GetLatestParsedDataFromDatabase() (*model.ParsedResult, error) {
|
||||||
if a.databaseService == nil {
|
if a.databaseService == nil {
|
||||||
return nil, fmt.Errorf("数据库服务未初始化")
|
return nil, fmt.Errorf("database service not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsJSON, heroesJSON, err := a.databaseService.GetLatestParsedDataFromDatabase()
|
itemsJSON, heroesJSON, err := a.databaseService.GetLatestParsedDataFromDatabase()
|
||||||
@@ -312,19 +323,17 @@ func (a *App) GetLatestParsedDataFromDatabase() (*model.ParsedResult, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析装备数据
|
|
||||||
var items []interface{}
|
var items []interface{}
|
||||||
if itemsJSON != "" {
|
if itemsJSON != "" {
|
||||||
if err := json.Unmarshal([]byte(itemsJSON), &items); err != nil {
|
if err := json.Unmarshal([]byte(itemsJSON), &items); err != nil {
|
||||||
return nil, fmt.Errorf("解析装备数据失败: %w", err)
|
return nil, fmt.Errorf("failed to unmarshal items: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析英雄数据
|
|
||||||
var heroes []interface{}
|
var heroes []interface{}
|
||||||
if heroesJSON != "" {
|
if heroesJSON != "" {
|
||||||
if err := json.Unmarshal([]byte(heroesJSON), &heroes); err != nil {
|
if err := json.Unmarshal([]byte(heroesJSON), &heroes); err != nil {
|
||||||
return nil, fmt.Errorf("解析英雄数据失败: %w", err)
|
return nil, fmt.Errorf("failed to unmarshal heroes: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,26 +343,125 @@ func (a *App) GetLatestParsedDataFromDatabase() (*model.ParsedResult, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveAppSetting 保存应用设置
|
// GetParsedSessions returns all parsed sessions.
|
||||||
|
func (a *App) GetParsedSessions() ([]model.ParsedSession, error) {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return nil, fmt.Errorf("database service not initialized")
|
||||||
|
}
|
||||||
|
return a.databaseService.GetParsedSessions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetParsedDataByID returns parsed data by session id.
|
||||||
|
func (a *App) GetParsedDataByID(id int64) (*model.ParsedResult, error) {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return nil, fmt.Errorf("database service not initialized")
|
||||||
|
}
|
||||||
|
itemsJSON, heroesJSON, err := a.databaseService.GetParsedDataByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var items []interface{}
|
||||||
|
if itemsJSON != "" {
|
||||||
|
if err := json.Unmarshal([]byte(itemsJSON), &items); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal items: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var heroes []interface{}
|
||||||
|
if heroesJSON != "" {
|
||||||
|
if err := json.Unmarshal([]byte(heroesJSON), &heroes); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal heroes: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.ParsedResult{
|
||||||
|
Items: items,
|
||||||
|
Heroes: heroes,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateParsedSessionName updates session name.
|
||||||
|
func (a *App) UpdateParsedSessionName(id int64, name string) error {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return fmt.Errorf("database service not initialized")
|
||||||
|
}
|
||||||
|
if name == "" {
|
||||||
|
return fmt.Errorf("session name cannot be empty")
|
||||||
|
}
|
||||||
|
return a.databaseService.UpdateParsedSessionName(id, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteParsedSession deletes a parsed session by id.
|
||||||
|
func (a *App) DeleteParsedSession(id int64) error {
|
||||||
|
if a.databaseService == nil {
|
||||||
|
return fmt.Errorf("database service not initialized")
|
||||||
|
}
|
||||||
|
return a.databaseService.DeleteParsedSession(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveAppSetting saves app setting.
|
||||||
func (a *App) SaveAppSetting(key, value string) error {
|
func (a *App) SaveAppSetting(key, value string) error {
|
||||||
if a.databaseService == nil {
|
if a.databaseService == nil {
|
||||||
return fmt.Errorf("数据库服务未初始化")
|
return fmt.Errorf("database service not initialized")
|
||||||
}
|
}
|
||||||
return a.databaseService.SaveAppSetting(key, value)
|
return a.databaseService.SaveAppSetting(key, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAppSetting 获取应用设置
|
// GetAppSetting gets app setting.
|
||||||
func (a *App) GetAppSetting(key string) (string, error) {
|
func (a *App) GetAppSetting(key string) (string, error) {
|
||||||
if a.databaseService == nil {
|
if a.databaseService == nil {
|
||||||
return "", fmt.Errorf("数据库服务未初始化")
|
return "", fmt.Errorf("database service not initialized")
|
||||||
}
|
}
|
||||||
return a.databaseService.GetAppSetting(key)
|
return a.databaseService.GetAppSetting(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllAppSettings 获取所有应用设置
|
// GetAllAppSettings gets all app settings.
|
||||||
func (a *App) GetAllAppSettings() (map[string]string, error) {
|
func (a *App) GetAllAppSettings() (map[string]string, error) {
|
||||||
if a.databaseService == nil {
|
if a.databaseService == nil {
|
||||||
return nil, fmt.Errorf("数据库服务未初始化")
|
return nil, fmt.Errorf("database service not initialized")
|
||||||
}
|
}
|
||||||
return a.databaseService.GetAllAppSettings()
|
return a.databaseService.GetAllAppSettings()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StartCaptureWithFilter allows frontend to provide a custom BPF filter.
|
||||||
|
func (a *App) StartCaptureWithFilter(interfaceName string, filter string) error {
|
||||||
|
a.logger.Info("StartCaptureWithFilter requested", "interface", interfaceName, "filter", filter)
|
||||||
|
if a.captureService.IsCapturing() {
|
||||||
|
return fmt.Errorf("capture already running")
|
||||||
|
}
|
||||||
|
|
||||||
|
useFilter := filter
|
||||||
|
if useFilter == "" {
|
||||||
|
useFilter = a.config.Capture.DefaultFilter
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutMs := a.config.Capture.DefaultTimeout
|
||||||
|
if timeoutMs > 500 {
|
||||||
|
log.Printf("[service] capture timeout too high (%dms), clamp to 500ms for responsive stop", timeoutMs)
|
||||||
|
timeoutMs = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
config := capture.Config{
|
||||||
|
InterfaceName: interfaceName,
|
||||||
|
Filter: useFilter,
|
||||||
|
Timeout: time.Duration(timeoutMs) * time.Millisecond,
|
||||||
|
BufferSize: a.config.Capture.BufferSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.captureService.StartCaptureAsync(context.Background(), config, func() {
|
||||||
|
if a.ctx != nil {
|
||||||
|
runtime.EventsEmit(a.ctx, "capture:started")
|
||||||
|
}
|
||||||
|
}, func(err error) {
|
||||||
|
a.logger.Error("start capture failed", "error", err)
|
||||||
|
if a.ctx != nil {
|
||||||
|
runtime.EventsEmit(a.ctx, "capture:start_failed", err.Error())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.logger.Info("capture start requested", "interface", interfaceName, "filter", useFilter)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user