first commit
This commit is contained in:
131
README.md
Normal file
131
README.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# NAS监控系统
|
||||
|
||||
一个实时显示系统信息的Web监控面板,支持CPU、内存、硬盘、网络等信息的实时监控。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🖥️ **实时系统监控**: CPU使用率、温度、内存使用情况
|
||||
- 💾 **存储监控**: 硬盘使用率、容量信息、多硬盘支持
|
||||
- 🌐 **网络监控**: 实时网络速度、IP地址管理
|
||||
- 🎨 **主题切换**: 支持明暗主题切换
|
||||
- ⚡ **高频更新**: 0.5秒更新频率,实时响应
|
||||
- 📱 **响应式设计**: 适配不同屏幕尺寸
|
||||
|
||||
## 系统要求
|
||||
|
||||
- Python 3.7+
|
||||
- Linux/Windows系统
|
||||
- 依赖包:`psutil`, `fastapi`, `uvicorn`
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 方法一:使用Python启动脚本(推荐)
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install psutil fastapi uvicorn
|
||||
|
||||
# 启动服务器
|
||||
python start_server.py
|
||||
```
|
||||
|
||||
### 方法二:使用Linux Shell脚本
|
||||
|
||||
```bash
|
||||
# 给脚本执行权限
|
||||
chmod +x start_server.sh
|
||||
|
||||
# 启动服务器
|
||||
./start_server.sh
|
||||
```
|
||||
|
||||
### 方法三:手动启动
|
||||
|
||||
```bash
|
||||
# 终端1:启动API服务器
|
||||
python system_info.py --serve --port 8001
|
||||
|
||||
# 终端2:启动HTTP服务器
|
||||
python -m http.server 8000
|
||||
```
|
||||
|
||||
## 访问地址
|
||||
|
||||
启动成功后,在浏览器中访问:
|
||||
|
||||
- **前端页面**: http://localhost:8000
|
||||
- **API接口**: http://localhost:8001/system-info
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
web/
|
||||
├── index.html # 前端页面
|
||||
├── system_info.py # 后端API服务器
|
||||
├── start_server.py # Python启动脚本(跨平台)
|
||||
├── start_server.sh # Linux Shell启动脚本
|
||||
├── README.md # 说明文档
|
||||
└── logs/ # 日志目录(自动创建)
|
||||
├── api_server.log # API服务器日志
|
||||
└── http_server.log # HTTP服务器日志
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. **启动服务器**: 使用上述任一方法启动服务器
|
||||
2. **访问页面**: 打开浏览器访问 http://localhost:8000
|
||||
3. **实时监控**: 页面每0.5秒自动更新系统信息
|
||||
4. **主题切换**: 点击左上角按钮切换明暗主题
|
||||
5. **IP选择**: 右上角可选择不同网络接口的IP地址
|
||||
6. **停止服务器**: 按 `Ctrl+C` 停止服务器
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 端口被占用
|
||||
|
||||
如果遇到端口被占用的错误,可以:
|
||||
|
||||
1. 检查端口使用情况:
|
||||
```bash
|
||||
# Linux
|
||||
netstat -tulpn | grep :8000
|
||||
netstat -tulpn | grep :8001
|
||||
|
||||
# Windows
|
||||
netstat -ano | findstr :8000
|
||||
netstat -ano | findstr :8001
|
||||
```
|
||||
|
||||
2. 修改端口(在脚本中修改端口号)
|
||||
|
||||
### 依赖安装失败
|
||||
|
||||
```bash
|
||||
# 使用国内镜像源
|
||||
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple psutil fastapi uvicorn
|
||||
|
||||
# 或使用conda
|
||||
conda install psutil
|
||||
pip install fastapi uvicorn
|
||||
```
|
||||
|
||||
### 权限问题(Linux)
|
||||
|
||||
```bash
|
||||
# 给脚本执行权限
|
||||
chmod +x start_server.sh
|
||||
|
||||
# 如果需要监控某些系统信息,可能需要sudo权限
|
||||
sudo python start_server.py
|
||||
```
|
||||
|
||||
## 开发说明
|
||||
|
||||
- **前端**: 纯HTML/CSS/JavaScript,无需构建工具
|
||||
- **后端**: Python FastAPI,提供RESTful API
|
||||
- **数据获取**: 使用psutil库获取系统信息
|
||||
- **更新频率**: 可在index.html中修改定时器间隔
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用MIT许可证。
|
||||
BIN
__pycache__/start_server.cpython-311.pyc
Normal file
BIN
__pycache__/start_server.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/start_server_venv.cpython-311.pyc
Normal file
BIN
__pycache__/start_server_venv.cpython-311.pyc
Normal file
Binary file not shown.
BIN
__pycache__/system_info.cpython-311.pyc
Normal file
BIN
__pycache__/system_info.cpython-311.pyc
Normal file
Binary file not shown.
4
config.json
Normal file
4
config.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"frontend_port": 8003,
|
||||
"backend_port": 8002
|
||||
}
|
||||
BIN
image/1.gif
Normal file
BIN
image/1.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 647 KiB |
BIN
image/2.gif
Normal file
BIN
image/2.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 MiB |
BIN
image/4.gif
Normal file
BIN
image/4.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 584 KiB |
BIN
image/二次元动漫美少女gif动图可爱表情包原神芙宁娜44_爱给网_aigei_com.gif
Normal file
BIN
image/二次元动漫美少女gif动图可爱表情包原神芙宁娜44_爱给网_aigei_com.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
BIN
image/原神表情贴纸头像 (2)_爱给网_aigei_com.gif
Normal file
BIN
image/原神表情贴纸头像 (2)_爱给网_aigei_com.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 MiB |
BIN
image/透明背景-二次元_开心_爱给网_aigei_com.gif
Normal file
BIN
image/透明背景-二次元_开心_爱给网_aigei_com.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
942
index.html
Normal file
942
index.html
Normal file
@@ -0,0 +1,942 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>系统监控仪表板</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-color: #ff69b4;
|
||||
--secondary-color: #ffb6c1;
|
||||
--background-color: #ffe4e1;
|
||||
--text-color: #333;
|
||||
--border-color: #000;
|
||||
--progress-bg: #f0f0f0;
|
||||
}
|
||||
|
||||
[data-theme="blue"] {
|
||||
--primary-color: #4169e1;
|
||||
--secondary-color: #87ceeb;
|
||||
--background-color: #e6f3ff;
|
||||
--text-color: #333;
|
||||
--border-color: #000;
|
||||
--progress-bg: #f0f0f0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* 主网格容器 */
|
||||
.grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(8, 1fr);
|
||||
grid-template-rows: repeat(7, 1fr);
|
||||
gap: 4px;
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
padding: 10px 30px;
|
||||
}
|
||||
|
||||
/* 组件基础样式 */
|
||||
.component {
|
||||
border: 2px solid var(--border-color);
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* 顶部组件样式 */
|
||||
.component-top {
|
||||
grid-row: 1 / 2;
|
||||
grid-column: 1 / 9;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.theme-toggle:hover {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.ip-selector {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ip-display {
|
||||
background: var(--secondary-color);
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.ip-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
background: white;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
min-width: 200px;
|
||||
display: none;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.ip-option {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.ip-option:hover {
|
||||
background: var(--background-color);
|
||||
}
|
||||
|
||||
/* 组件1: 时钟和网络 */
|
||||
.component-1 {
|
||||
grid-row: 2 / 6;
|
||||
grid-column: 1 / 3;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.clock {
|
||||
font-size: 3.75em;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.network-info {
|
||||
font-size: 1.35em;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.network-speed {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
/* 组件2: CPU */
|
||||
.component-2 {
|
||||
grid-row: 2 / 4;
|
||||
grid-column: 3 / 7;
|
||||
}
|
||||
|
||||
/* 组件3: 内存 */
|
||||
.component-3 {
|
||||
grid-row: 4 / 6;
|
||||
grid-column: 3 / 7;
|
||||
}
|
||||
|
||||
/* 进度条样式 */
|
||||
.progress-container {
|
||||
position: relative;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
background: var(--progress-bg);
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
||||
border-radius: 10px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: black;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 组件4: 硬盘总容量圆形进度条 */
|
||||
.component-4 {
|
||||
grid-row: 2 / 6;
|
||||
grid-column: 7 / 9;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.circular-progress {
|
||||
position: relative;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.circular-progress svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.circular-progress circle {
|
||||
fill: none;
|
||||
stroke-width: 8;
|
||||
}
|
||||
|
||||
.progress-bg-circle {
|
||||
stroke: var(--progress-bg);
|
||||
}
|
||||
|
||||
.progress-circle {
|
||||
stroke: var(--primary-color);
|
||||
stroke-linecap: round;
|
||||
transition: stroke-dasharray 0.5s ease;
|
||||
}
|
||||
|
||||
.circular-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
font-size: 21px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
/* 组件5-8: 按钮 */
|
||||
.component-5, .component-6, .component-7, .component-8 {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
|
||||
color: white;
|
||||
}
|
||||
|
||||
.component-5 {
|
||||
grid-row: 6 / 7;
|
||||
grid-column: 1 / 2;
|
||||
}
|
||||
|
||||
.component-6 {
|
||||
grid-row: 6 / 7;
|
||||
grid-column: 2 / 3;
|
||||
}
|
||||
|
||||
.component-7 {
|
||||
grid-row: 7 / 8;
|
||||
grid-column: 1 / 2;
|
||||
}
|
||||
|
||||
.component-8 {
|
||||
grid-row: 7 / 8;
|
||||
grid-column: 2 / 3;
|
||||
}
|
||||
|
||||
.component-5:hover, .component-6:hover, .component-7:hover, .component-8:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
/* 组件9: GIF展示区域 */
|
||||
.component-9 {
|
||||
grid-row: 6 / 8;
|
||||
grid-column: 3 / 7;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #f8f8f8;
|
||||
border-style: dashed;
|
||||
cursor: pointer;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.gif-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gif-item {
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.gif-item:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.gif-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 组件10: 硬盘列表 */
|
||||
.component-10 {
|
||||
grid-row: 6 / 8;
|
||||
grid-column: 7 / 9;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.disk-summary {
|
||||
padding: 10px;
|
||||
background: var(--background-color);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.disk-expanded {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: white;
|
||||
border: 3px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
z-index: 2000;
|
||||
min-width: 400px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
.disk-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0,0,0,0.5);
|
||||
z-index: 1999;
|
||||
}
|
||||
|
||||
.disk-item {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px;
|
||||
background: var(--background-color);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.disk-name {
|
||||
font-size: 12px;
|
||||
margin-bottom: 5px;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.disk-progress {
|
||||
height: 15px;
|
||||
background: var(--progress-bg);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.disk-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.disk-info {
|
||||
font-size: 10px;
|
||||
margin-top: 3px;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.disk-expanded-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.disk-expanded-header h3 {
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.disk-detail-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.disk-detail-list .disk-item {
|
||||
background: var(--background-color);
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.fullscreen-btn {
|
||||
background: var(--secondary-color);
|
||||
color: var(--text-color);
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.fullscreen-btn:hover {
|
||||
opacity: 0.8;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="grid-container">
|
||||
<!-- 顶部组件: 主题切换、全屏和IP选择 -->
|
||||
<div class="component component-top">
|
||||
<div>
|
||||
<button class="theme-toggle" onclick="toggleTheme()">🎨 主题切换</button>
|
||||
<button class="fullscreen-btn" onclick="toggleFullscreen()">🔳 全屏</button>
|
||||
</div>
|
||||
<div class="ip-selector">
|
||||
<div class="ip-display" onclick="toggleIpDropdown()" id="currentIp">192.168.101.102</div>
|
||||
<div class="ip-dropdown" id="ipDropdown">
|
||||
<!-- IP选项将通过JavaScript动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 组件1: 时钟和网络 -->
|
||||
<div class="component component-1">
|
||||
<div class="clock" id="clock">20:36</div>
|
||||
<div class="network-info">
|
||||
<div class="network-speed">↑ <span id="uploadSpeed">0.354</span> MB/s</div>
|
||||
<div class="network-speed">↓ <span id="downloadSpeed">0.111</span> MB/s</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 组件2: CPU -->
|
||||
<div class="component component-2">
|
||||
<div class="progress-header">
|
||||
<span>🖥️ CPU</span>
|
||||
<span id="cpuTemp">--°C</span>
|
||||
</div>
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="cpuProgress" style="width: 11.9%"></div>
|
||||
<div class="progress-text" id="cpuText">11.9% 使用中</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 组件3: 内存 -->
|
||||
<div class="component component-3">
|
||||
<div class="progress-header">
|
||||
<span>📊 RAM</span>
|
||||
<span id="memoryInfo">19.53GB / 63.93GB</span>
|
||||
</div>
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="memoryProgress" style="width: 30.5%"></div>
|
||||
<div class="progress-text" id="memoryText">30.5% 使用中</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 组件4: 硬盘总容量 -->
|
||||
<div class="component component-4">
|
||||
<h3 style="text-align: center; margin-bottom: 15px;">💾 总容量</h3>
|
||||
<div class="circular-progress">
|
||||
<svg>
|
||||
<circle class="progress-bg-circle" cx="60" cy="60" r="52"></circle>
|
||||
<circle class="progress-circle" cx="60" cy="60" r="52" id="totalDiskCircle"></circle>
|
||||
</svg>
|
||||
<div class="circular-text">
|
||||
<div id="totalDiskPercent">77%</div>
|
||||
<div style="font-size: 10px; margin-top: 5px;" id="totalDiskInfo">1469GB / 1908GB</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 组件5-8: 功能按钮 -->
|
||||
<div class="component component-5">按钮1</div>
|
||||
<div class="component component-6">按钮2</div>
|
||||
<div class="component component-7">按钮3</div>
|
||||
<div class="component component-8">按钮4</div>
|
||||
|
||||
<!-- 组件9: GIF展示区域 -->
|
||||
<div class="component component-9" id="component9">
|
||||
<div class="gif-container" id="gifContainer">
|
||||
<div class="gif-item" onclick="switchSingleGif(0)">
|
||||
<img id="gif1" src="" alt="GIF 1">
|
||||
</div>
|
||||
<div class="gif-item" onclick="switchSingleGif(1)">
|
||||
<img id="gif2" src="" alt="GIF 2">
|
||||
</div>
|
||||
<div class="gif-item" onclick="switchSingleGif(2)">
|
||||
<img id="gif3" src="" alt="GIF 3">
|
||||
</div>
|
||||
<div class="gif-item" onclick="switchSingleGif(3)">
|
||||
<img id="gif4" src="" alt="GIF 4">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 组件10: 硬盘列表 -->
|
||||
<div class="component component-10" onclick="showDiskDetails()">
|
||||
<div id="diskSummary">
|
||||
<!-- 硬盘摘要将通过JavaScript动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 硬盘详情弹窗 -->
|
||||
<div id="diskOverlay" class="disk-overlay" style="display: none;" onclick="hideDiskDetails()"></div>
|
||||
<div id="diskExpanded" class="disk-expanded" style="display: none;">
|
||||
<div class="disk-expanded-header">
|
||||
<button onclick="hideDiskDetails()" class="close-btn">×</button>
|
||||
</div>
|
||||
<div id="diskDetailList" class="disk-detail-list">
|
||||
<!-- 硬盘详细列表将通过JavaScript动态生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// GIF文件列表(动态从服务器获取)
|
||||
let gifFiles = [];
|
||||
let currentGifs = [null, null, null, null];
|
||||
|
||||
// 配置信息
|
||||
let config = {
|
||||
frontend_port: 8003,
|
||||
backend_port: 8002
|
||||
};
|
||||
|
||||
/**
|
||||
* 从配置文件获取端口配置
|
||||
*/
|
||||
async function fetchConfig() {
|
||||
try {
|
||||
const response = await fetch('/config.json');
|
||||
if (response.ok) {
|
||||
config = await response.json();
|
||||
console.log('配置加载成功:', config);
|
||||
} else {
|
||||
console.warn('配置文件加载失败,使用默认配置');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('配置文件读取出错,使用默认配置:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从API获取图片文件列表
|
||||
*/
|
||||
async function fetchImageFiles() {
|
||||
try {
|
||||
const currentHost = window.location.hostname;
|
||||
const apiUrl = `http://${currentHost}:${config.backend_port}/image-files`;
|
||||
|
||||
const response = await fetch(apiUrl);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
gifFiles = data.files || [];
|
||||
console.log('图片文件列表更新成功:', gifFiles);
|
||||
return true;
|
||||
} else {
|
||||
console.error('获取图片文件列表失败:', response.status);
|
||||
// 使用默认文件列表作为备用
|
||||
gifFiles = ['1.gif', '2.gif', '4.gif'];
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取图片文件列表出错:', error);
|
||||
// 使用默认文件列表作为备用
|
||||
gifFiles = ['1.gif', '2.gif', '4.gif'];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 系统数据存储变量
|
||||
let systemData = {};
|
||||
|
||||
/**
|
||||
* 从API获取系统数据
|
||||
*/
|
||||
async function fetchSystemData() {
|
||||
try {
|
||||
// 动态获取当前页面的主机地址,支持局域网访问
|
||||
const currentHost = window.location.hostname;
|
||||
const apiUrl = `http://${currentHost}:${config.backend_port}/system-info`;
|
||||
|
||||
const response = await fetch(apiUrl);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
systemData = data;
|
||||
console.log('系统数据更新成功:', data);
|
||||
return true;
|
||||
} else {
|
||||
console.error('API响应错误:', response.status);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取系统数据失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化页面数据显示
|
||||
*/
|
||||
async function initializeData() {
|
||||
// 先获取配置
|
||||
await fetchConfig();
|
||||
|
||||
// 然后获取图片文件列表
|
||||
await fetchImageFiles();
|
||||
|
||||
const success = await fetchSystemData();
|
||||
if (success) {
|
||||
updateCPUInfo();
|
||||
updateMemoryInfo();
|
||||
updateDiskInfo();
|
||||
updateDiskSummary();
|
||||
updateNetworkInfo();
|
||||
generateIPDropdown();
|
||||
}
|
||||
updateClock();
|
||||
initializeGifs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化GIF显示
|
||||
*/
|
||||
function initializeGifs() {
|
||||
// 为每个位置随机分配GIF
|
||||
for (let i = 0; i < 4; i++) {
|
||||
switchSingleGif(i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机切换单个GIF图片
|
||||
* @param {number} index - GIF位置索引 (0-3)
|
||||
*/
|
||||
function switchSingleGif(index) {
|
||||
// 获取当前图片文件名
|
||||
const currentGif = currentGifs[index];
|
||||
|
||||
// 创建可选择的GIF列表(排除当前图片)
|
||||
const availableGifs = gifFiles.filter(gif => gif !== currentGif);
|
||||
|
||||
// 如果没有其他可选择的图片,则不进行切换
|
||||
if (availableGifs.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 从可选择的图片中随机选择一个
|
||||
const randomIndex = Math.floor(Math.random() * availableGifs.length);
|
||||
const selectedGif = availableGifs[randomIndex];
|
||||
|
||||
// 更新对应位置的GIF
|
||||
currentGifs[index] = selectedGif;
|
||||
document.getElementById(`gif${index + 1}`).src = `image/${selectedGif}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新CPU信息显示
|
||||
*/
|
||||
function updateCPUInfo() {
|
||||
if (!systemData.cpu) return;
|
||||
|
||||
const cpuUsage = systemData.cpu.usage_percent;
|
||||
const cpuTemp = systemData.cpu.temperature_c;
|
||||
|
||||
document.getElementById('cpuProgress').style.width = cpuUsage + '%';
|
||||
document.getElementById('cpuText').textContent = cpuUsage + '% 使用中';
|
||||
document.getElementById('cpuTemp').textContent = cpuTemp ? cpuTemp + '°C' : '--°C';
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新内存信息显示
|
||||
*/
|
||||
function updateMemoryInfo() {
|
||||
if (!systemData.memory) return;
|
||||
|
||||
const used = systemData.memory.used_gb;
|
||||
const total = systemData.memory.total_gb;
|
||||
const percentage = ((used / total) * 100).toFixed(1);
|
||||
|
||||
document.getElementById('memoryProgress').style.width = percentage + '%';
|
||||
document.getElementById('memoryText').textContent = percentage + '% 使用中';
|
||||
document.getElementById('memoryInfo').textContent = used + 'GB / ' + total + 'GB';
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新硬盘信息显示
|
||||
*/
|
||||
function updateDiskInfo() {
|
||||
if (!systemData.storage || !systemData.storage.disks) return;
|
||||
|
||||
// 更新总容量圆形进度条
|
||||
const totalUsed = systemData.storage.disks.reduce((sum, disk) => sum + disk.used_gb, 0);
|
||||
const totalCapacity = systemData.storage.disks.reduce((sum, disk) => sum + disk.total_gb, 0);
|
||||
const totalPercentage = ((totalUsed / totalCapacity) * 100).toFixed(0);
|
||||
|
||||
const circumference = 2 * Math.PI * 52;
|
||||
const offset = circumference - (totalPercentage / 100) * circumference;
|
||||
|
||||
const circle = document.getElementById('totalDiskCircle');
|
||||
circle.style.strokeDasharray = circumference;
|
||||
circle.style.strokeDashoffset = offset;
|
||||
|
||||
document.getElementById('totalDiskPercent').textContent = totalPercentage + '%';
|
||||
document.getElementById('totalDiskInfo').textContent = totalUsed.toFixed(0) + 'GB / ' + totalCapacity.toFixed(0) + 'GB';
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新硬盘摘要显示(仅显示第一块硬盘)
|
||||
*/
|
||||
function updateDiskSummary() {
|
||||
if (!systemData.storage || !systemData.storage.disks) return;
|
||||
|
||||
const diskSummary = document.getElementById('diskSummary');
|
||||
if (systemData.storage.disks.length > 0) {
|
||||
const firstDisk = systemData.storage.disks[0];
|
||||
const percentage = ((firstDisk.used_gb / firstDisk.total_gb) * 100).toFixed(1);
|
||||
|
||||
diskSummary.innerHTML = `
|
||||
<div class="disk-summary">
|
||||
<div class="disk-name">${firstDisk.name}</div>
|
||||
<div class="disk-progress">
|
||||
<div class="disk-progress-fill" style="width: ${percentage}%"></div>
|
||||
</div>
|
||||
<div class="disk-info">${firstDisk.used_gb.toFixed(1)}GB / ${firstDisk.total_gb.toFixed(1)}GB (${percentage}%)</div>
|
||||
<div style="font-size: 10px; color: #666; margin-top: 5px;">点击查看全部硬盘</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示硬盘详情弹窗
|
||||
*/
|
||||
function showDiskDetails() {
|
||||
const diskDetailList = document.getElementById('diskDetailList');
|
||||
diskDetailList.innerHTML = '';
|
||||
|
||||
systemData.storage.disks.forEach((disk, index) => {
|
||||
const percentage = ((disk.used_gb / disk.total_gb) * 100).toFixed(1);
|
||||
const diskItem = document.createElement('div');
|
||||
diskItem.className = 'disk-item';
|
||||
diskItem.innerHTML = `
|
||||
<div class="disk-name">${disk.name} (硬盘 ${index + 1})</div>
|
||||
<div class="disk-progress">
|
||||
<div class="disk-progress-fill" style="width: ${percentage}%"></div>
|
||||
</div>
|
||||
<div class="disk-info">${disk.used_gb.toFixed(1)}GB / ${disk.total_gb.toFixed(1)}GB (${percentage}%)</div>
|
||||
`;
|
||||
diskDetailList.appendChild(diskItem);
|
||||
});
|
||||
|
||||
document.getElementById('diskOverlay').style.display = 'block';
|
||||
document.getElementById('diskExpanded').style.display = 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏硬盘详情弹窗
|
||||
*/
|
||||
function hideDiskDetails() {
|
||||
document.getElementById('diskOverlay').style.display = 'none';
|
||||
document.getElementById('diskExpanded').style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新网络信息显示
|
||||
*/
|
||||
function updateNetworkInfo() {
|
||||
if (!systemData.network) return;
|
||||
|
||||
document.getElementById('uploadSpeed').textContent = systemData.network.up_mbps.toFixed(3);
|
||||
document.getElementById('downloadSpeed').textContent = systemData.network.down_mbps.toFixed(3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成IP地址下拉菜单
|
||||
*/
|
||||
function generateIPDropdown() {
|
||||
if (!systemData.network || !systemData.network.interfaces) return;
|
||||
|
||||
const dropdown = document.getElementById('ipDropdown');
|
||||
dropdown.innerHTML = '';
|
||||
|
||||
systemData.network.interfaces.forEach(interface => {
|
||||
const option = document.createElement('div');
|
||||
option.className = 'ip-option';
|
||||
option.innerHTML = `<strong>${interface.name}</strong><br>${interface.ip}`;
|
||||
option.onclick = () => selectIP(interface.ip);
|
||||
dropdown.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择IP地址
|
||||
*/
|
||||
function selectIP(ip) {
|
||||
document.getElementById('currentIp').textContent = ip;
|
||||
document.getElementById('ipDropdown').style.display = 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换IP下拉菜单显示状态
|
||||
*/
|
||||
function toggleIpDropdown() {
|
||||
const dropdown = document.getElementById('ipDropdown');
|
||||
dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block';
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题切换功能
|
||||
*/
|
||||
function toggleTheme() {
|
||||
const body = document.body;
|
||||
if (body.getAttribute('data-theme') === 'blue') {
|
||||
body.removeAttribute('data-theme');
|
||||
localStorage.setItem('theme', 'pink');
|
||||
} else {
|
||||
body.setAttribute('data-theme', 'blue');
|
||||
localStorage.setItem('theme', 'blue');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新时钟显示
|
||||
*/
|
||||
function updateClock() {
|
||||
const now = new Date();
|
||||
const hours = now.getHours().toString().padStart(2, '0');
|
||||
const minutes = now.getMinutes().toString().padStart(2, '0');
|
||||
document.getElementById('clock').textContent = hours + ':' + minutes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载保存的主题
|
||||
*/
|
||||
function loadTheme() {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
if (savedTheme === 'blue') {
|
||||
document.body.setAttribute('data-theme', 'blue');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 全屏切换功能
|
||||
*/
|
||||
function toggleFullscreen() {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen().catch(err => {
|
||||
console.log('无法进入全屏模式:', err);
|
||||
});
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
// 点击其他地方关闭IP下拉菜单
|
||||
document.addEventListener('click', function(event) {
|
||||
const ipSelector = document.querySelector('.ip-selector');
|
||||
if (!ipSelector.contains(event.target)) {
|
||||
document.getElementById('ipDropdown').style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', async function() {
|
||||
loadTheme();
|
||||
await initializeData();
|
||||
|
||||
// 每秒更新时钟
|
||||
setInterval(updateClock, 1000);
|
||||
|
||||
// 每0.5秒从API获取最新数据
|
||||
setInterval(async function() {
|
||||
const success = await fetchSystemData();
|
||||
if (success) {
|
||||
updateCPUInfo();
|
||||
updateMemoryInfo();
|
||||
updateDiskInfo();
|
||||
updateDiskSummary();
|
||||
updateNetworkInfo();
|
||||
generateIPDropdown();
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1019
logs/api_server.log
Normal file
1019
logs/api_server.log
Normal file
File diff suppressed because it is too large
Load Diff
2
logs/http_server.log
Normal file
2
logs/http_server.log
Normal file
@@ -0,0 +1,2 @@
|
||||
'"' <20><><EFBFBD><EFBFBD><EFBFBD>ڲ<EFBFBD><DAB2><EFBFBD><EFBFBD>ⲿ<EFBFBD><E2B2BF><EFBFBD>Ҳ<EEA3AC><D2B2><EFBFBD>ǿ<EFBFBD><C7BF><EFBFBD><EFBFBD>еij<D0B5><C4B3><EFBFBD>
|
||||
<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ļ<EFBFBD><EFBFBD><EFBFBD>
|
||||
13
requirements.txt
Normal file
13
requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
# NAS监控系统依赖包
|
||||
# 使用方法: pip install -r requirements.txt
|
||||
|
||||
# 系统信息获取
|
||||
psutil>=5.8.0
|
||||
|
||||
# Web框架和API服务
|
||||
fastapi>=0.68.0
|
||||
uvicorn[standard]>=0.15.0
|
||||
|
||||
# 可选依赖(用于更好的性能)
|
||||
# python-multipart>=0.0.5 # 用于处理表单数据
|
||||
# aiofiles>=0.7.0 # 异步文件操作
|
||||
262
start_server_venv.py
Normal file
262
start_server_venv.py
Normal file
@@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
NAS监控系统启动脚本 - 虚拟环境版本
|
||||
功能:在虚拟环境中启动Python API后端服务器和HTML静态文件服务器
|
||||
适用于Linux/Windows系统
|
||||
|
||||
使用方法:
|
||||
python3 start_server_venv.py
|
||||
或
|
||||
python start_server_venv.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
import subprocess
|
||||
import threading
|
||||
import venv
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class VenvServerManager:
|
||||
"""虚拟环境服务器管理类"""
|
||||
|
||||
def __init__(self):
|
||||
self.api_process = None
|
||||
self.http_process = None
|
||||
self.script_dir = Path(__file__).parent.absolute()
|
||||
self.logs_dir = self.script_dir / "logs"
|
||||
self.venv_dir = self.script_dir / "venv"
|
||||
self.venv_python = None
|
||||
|
||||
def setup_logging(self):
|
||||
"""创建日志目录"""
|
||||
self.logs_dir.mkdir(exist_ok=True)
|
||||
print(f"日志目录: {self.logs_dir}")
|
||||
|
||||
def create_virtual_environment(self):
|
||||
"""创建虚拟环境"""
|
||||
if self.venv_dir.exists():
|
||||
print(f"虚拟环境已存在: {self.venv_dir}")
|
||||
else:
|
||||
print(f"正在创建虚拟环境: {self.venv_dir}")
|
||||
try:
|
||||
venv.create(self.venv_dir, with_pip=True)
|
||||
print("虚拟环境创建成功")
|
||||
except Exception as e:
|
||||
print(f"错误: 创建虚拟环境失败 - {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# 设置虚拟环境Python路径
|
||||
if os.name == 'nt': # Windows
|
||||
self.venv_python = self.venv_dir / "Scripts" / "python.exe"
|
||||
else: # Linux/macOS
|
||||
self.venv_python = self.venv_dir / "bin" / "python"
|
||||
|
||||
if not self.venv_python.exists():
|
||||
print(f"错误: 虚拟环境Python不存在: {self.venv_python}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"虚拟环境Python: {self.venv_python}")
|
||||
|
||||
def install_dependencies(self):
|
||||
"""在虚拟环境中安装依赖包"""
|
||||
print("检查并安装Python依赖包...")
|
||||
|
||||
# 检查依赖是否已安装
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[str(self.venv_python), "-c", "import psutil, fastapi, uvicorn"],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
print("依赖包已安装")
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 安装依赖包
|
||||
print("正在安装依赖包...")
|
||||
try:
|
||||
# 获取pip路径
|
||||
if os.name == 'nt': # Windows
|
||||
pip_path = self.venv_dir / "Scripts" / "pip.exe"
|
||||
else: # Linux/macOS
|
||||
pip_path = self.venv_dir / "bin" / "pip"
|
||||
|
||||
subprocess.run(
|
||||
[str(pip_path), "install", "psutil", "fastapi", "uvicorn"],
|
||||
check=True,
|
||||
cwd=self.script_dir
|
||||
)
|
||||
print("依赖包安装成功")
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"错误: 依赖包安装失败 - {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def check_dependencies(self):
|
||||
"""检查依赖文件"""
|
||||
print("检查依赖文件...")
|
||||
|
||||
# 检查必要文件
|
||||
required_files = ["system_info.py", "index.html"]
|
||||
for file in required_files:
|
||||
if not (self.script_dir / file).exists():
|
||||
print(f"错误: 未找到{file}文件")
|
||||
sys.exit(1)
|
||||
|
||||
def start_api_server(self):
|
||||
"""启动API服务器"""
|
||||
print("启动Python API服务器 (端口8001)...")
|
||||
|
||||
api_log = self.logs_dir / "api_server.log"
|
||||
with open(api_log, "w", encoding="utf-8") as log_file:
|
||||
self.api_process = subprocess.Popen(
|
||||
[str(self.venv_python), "system_info.py", "--serve", "--port", "8001"],
|
||||
cwd=self.script_dir,
|
||||
stdout=log_file,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True
|
||||
)
|
||||
|
||||
print(f"API服务器已启动 (PID: {self.api_process.pid})")
|
||||
return self.api_process
|
||||
|
||||
def start_http_server(self):
|
||||
"""启动HTTP静态文件服务器"""
|
||||
print("启动HTTP静态文件服务器 (端口8000)...")
|
||||
|
||||
http_log = self.logs_dir / "http_server.log"
|
||||
with open(http_log, "w", encoding="utf-8") as log_file:
|
||||
self.http_process = subprocess.Popen(
|
||||
[str(self.venv_python), "-m", "http.server", "8000"],
|
||||
cwd=self.script_dir,
|
||||
stdout=log_file,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True
|
||||
)
|
||||
|
||||
print(f"HTTP服务器已启动 (PID: {self.http_process.pid})")
|
||||
return self.http_process
|
||||
|
||||
def check_server_health(self):
|
||||
"""检查服务器健康状态"""
|
||||
time.sleep(3) # 等待服务器启动
|
||||
|
||||
# 检查API服务器
|
||||
if self.api_process.poll() is not None:
|
||||
print("错误: API服务器启动失败,请检查logs/api_server.log")
|
||||
return False
|
||||
|
||||
# 检查HTTP服务器
|
||||
if self.http_process.poll() is not None:
|
||||
print("错误: HTTP服务器启动失败,请检查logs/http_server.log")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def monitor_servers(self):
|
||||
"""监控服务器状态"""
|
||||
while True:
|
||||
time.sleep(5)
|
||||
|
||||
# 检查API服务器
|
||||
if self.api_process.poll() is not None:
|
||||
print("警告: API服务器进程已停止")
|
||||
break
|
||||
|
||||
# 检查HTTP服务器
|
||||
if self.http_process.poll() is not None:
|
||||
print("警告: HTTP服务器进程已停止")
|
||||
break
|
||||
|
||||
def cleanup(self):
|
||||
"""清理资源"""
|
||||
print("\n正在停止服务器...")
|
||||
|
||||
if self.api_process:
|
||||
try:
|
||||
self.api_process.terminate()
|
||||
self.api_process.wait(timeout=5)
|
||||
print(f"已停止API服务器 (PID: {self.api_process.pid})")
|
||||
except subprocess.TimeoutExpired:
|
||||
self.api_process.kill()
|
||||
print(f"强制停止API服务器 (PID: {self.api_process.pid})")
|
||||
except Exception as e:
|
||||
print(f"停止API服务器时出错: {e}")
|
||||
|
||||
if self.http_process:
|
||||
try:
|
||||
self.http_process.terminate()
|
||||
self.http_process.wait(timeout=5)
|
||||
print(f"已停止HTTP服务器 (PID: {self.http_process.pid})")
|
||||
except subprocess.TimeoutExpired:
|
||||
self.http_process.kill()
|
||||
print(f"强制停止HTTP服务器 (PID: {self.http_process.pid})")
|
||||
except Exception as e:
|
||||
print(f"停止HTTP服务器时出错: {e}")
|
||||
|
||||
print("服务器已停止")
|
||||
|
||||
def signal_handler(self, signum, frame):
|
||||
"""信号处理函数"""
|
||||
print(f"\n收到信号 {signum},正在关闭服务器...")
|
||||
self.cleanup()
|
||||
sys.exit(0)
|
||||
|
||||
def run(self):
|
||||
"""运行服务器管理器"""
|
||||
print("=== NAS监控系统启动脚本(虚拟环境版本) ===")
|
||||
print(f"工作目录: {self.script_dir}")
|
||||
|
||||
# 设置信号处理
|
||||
signal.signal(signal.SIGINT, self.signal_handler)
|
||||
if hasattr(signal, 'SIGTERM'):
|
||||
signal.signal(signal.SIGTERM, self.signal_handler)
|
||||
|
||||
try:
|
||||
# 初始化
|
||||
self.setup_logging()
|
||||
self.create_virtual_environment()
|
||||
self.install_dependencies()
|
||||
self.check_dependencies()
|
||||
|
||||
# 启动服务器
|
||||
self.start_api_server()
|
||||
self.start_http_server()
|
||||
|
||||
# 检查服务器健康状态
|
||||
if not self.check_server_health():
|
||||
self.cleanup()
|
||||
sys.exit(1)
|
||||
|
||||
print("\n=== 服务器启动成功 ===")
|
||||
print(f"虚拟环境: {self.venv_dir}")
|
||||
print("前端页面: http://localhost:8000")
|
||||
print("API接口: http://localhost:8001/system-info")
|
||||
print("日志文件: logs/api_server.log, logs/http_server.log")
|
||||
print("\n按 Ctrl+C 停止服务器")
|
||||
|
||||
# 监控服务器
|
||||
self.monitor_servers()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n用户中断")
|
||||
except Exception as e:
|
||||
print(f"\n运行时错误: {e}")
|
||||
finally:
|
||||
self.cleanup()
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
manager = VenvServerManager()
|
||||
manager.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
225
start_server_venv.sh
Normal file
225
start_server_venv.sh
Normal file
@@ -0,0 +1,225 @@
|
||||
#!/bin/bash
|
||||
# NAS监控系统启动脚本 - 虚拟环境版本
|
||||
# 功能:在虚拟环境中启动Python API后端服务器和HTML静态文件服务器
|
||||
|
||||
# 设置脚本目录为工作目录
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "=== NAS监控系统启动脚本(虚拟环境版本) ==="
|
||||
echo "工作目录: $SCRIPT_DIR"
|
||||
|
||||
# 读取配置文件
|
||||
CONFIG_FILE="$SCRIPT_DIR/config.json"
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
echo "错误: 配置文件config.json不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 从JSON配置文件读取端口
|
||||
FRONTEND_PORT=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE'))['frontend_port'])" 2>/dev/null)
|
||||
BACKEND_PORT=$(python3 -c "import json; print(json.load(open('$CONFIG_FILE'))['backend_port'])" 2>/dev/null)
|
||||
|
||||
if [ -z "$FRONTEND_PORT" ] || [ -z "$BACKEND_PORT" ]; then
|
||||
echo "错误: 无法从config.json读取端口配置"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "配置端口 - 前端: $FRONTEND_PORT, 后端: $BACKEND_PORT"
|
||||
|
||||
# 虚拟环境目录
|
||||
VENV_DIR="$SCRIPT_DIR/venv"
|
||||
|
||||
# 检查虚拟环境是否存在
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo "虚拟环境不存在,正在创建..."
|
||||
|
||||
# 检查Python是否安装
|
||||
if ! which python3 >/dev/null 2>&1 && ! python3 --version >/dev/null 2>&1; then
|
||||
echo "错误: 未找到python3,请先安装Python 3"
|
||||
echo "请尝试: sudo apt update && sudo apt install python3 python3-venv python3-pip"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "找到Python: $(python3 --version)"
|
||||
|
||||
# 创建虚拟环境
|
||||
python3 -m venv "$VENV_DIR" || {
|
||||
echo "错误: 创建虚拟环境失败"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "虚拟环境创建成功: $VENV_DIR"
|
||||
fi
|
||||
|
||||
# 激活虚拟环境
|
||||
echo "激活虚拟环境..."
|
||||
. "$VENV_DIR/bin/activate" || {
|
||||
echo "错误: 激活虚拟环境失败"
|
||||
exit 1
|
||||
}
|
||||
|
||||
echo "当前Python路径: $(which python)"
|
||||
echo "当前Python版本: $(python --version)"
|
||||
|
||||
# 检查必要文件是否存在
|
||||
if [ ! -f "system_info.py" ]; then
|
||||
echo "错误: 未找到system_info.py文件"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "index.html" ]; then
|
||||
echo "错误: 未找到index.html文件"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查并安装Python依赖
|
||||
echo "检查Python依赖..."
|
||||
if ! python -c "import psutil, fastapi, uvicorn" 2>/dev/null; then
|
||||
echo "安装Python依赖包..."
|
||||
pip install psutil fastapi uvicorn || {
|
||||
echo "错误: 依赖安装失败,请检查网络连接"
|
||||
exit 1
|
||||
}
|
||||
else
|
||||
echo "依赖包已安装"
|
||||
fi
|
||||
|
||||
# 创建日志目录
|
||||
mkdir -p logs
|
||||
|
||||
# 定义清理函数
|
||||
cleanup() {
|
||||
echo "\n正在停止服务器..."
|
||||
|
||||
# 停止API服务器
|
||||
if [ ! -z "$API_PID" ]; then
|
||||
kill $API_PID 2>/dev/null
|
||||
sleep 1
|
||||
# 如果进程仍在运行,强制终止
|
||||
if kill -0 $API_PID 2>/dev/null; then
|
||||
kill -9 $API_PID 2>/dev/null
|
||||
fi
|
||||
echo "已停止API服务器 (PID: $API_PID)"
|
||||
fi
|
||||
|
||||
# 停止HTTP服务器
|
||||
if [ ! -z "$HTTP_PID" ]; then
|
||||
kill $HTTP_PID 2>/dev/null
|
||||
sleep 1
|
||||
# 如果进程仍在运行,强制终止
|
||||
if kill -0 $HTTP_PID 2>/dev/null; then
|
||||
kill -9 $HTTP_PID 2>/dev/null
|
||||
fi
|
||||
echo "已停止HTTP服务器 (PID: $HTTP_PID)"
|
||||
fi
|
||||
|
||||
# 额外检查:杀死可能残留的Python HTTP服务器进程
|
||||
pkill -f "python.*http.server.*$FRONTEND_PORT" 2>/dev/null || true
|
||||
|
||||
echo "退出虚拟环境"
|
||||
if command -v deactivate >/dev/null 2>&1; then
|
||||
deactivate 2>/dev/null
|
||||
fi
|
||||
echo "服务器已停止"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# 设置信号处理
|
||||
trap cleanup INT TERM
|
||||
|
||||
# 检查端口是否被占用的函数
|
||||
check_port() {
|
||||
local port=$1
|
||||
if netstat -tuln 2>/dev/null | grep -q ":$port " || ss -tuln 2>/dev/null | grep -q ":$port "; then
|
||||
return 0 # 端口被占用
|
||||
else
|
||||
return 1 # 端口可用
|
||||
fi
|
||||
}
|
||||
|
||||
# 清理可能残留的进程
|
||||
echo "清理可能残留的进程..."
|
||||
pkill -f "python.*system_info.py.*--serve" 2>/dev/null || true
|
||||
pkill -f "python.*http.server.*$FRONTEND_PORT" 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
echo "启动服务器..."
|
||||
|
||||
# 检查API端口
|
||||
if check_port $BACKEND_PORT; then
|
||||
echo "警告: 端口$BACKEND_PORT被占用,尝试清理..."
|
||||
pkill -f "python.*system_info.py.*--serve" 2>/dev/null || true
|
||||
sleep 2
|
||||
if check_port $BACKEND_PORT; then
|
||||
echo "错误: 端口$BACKEND_PORT仍被占用,请手动检查"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 启动Python API后端服务器
|
||||
echo "启动Python API服务器 (端口$BACKEND_PORT)..."
|
||||
python system_info.py --serve --port $BACKEND_PORT > logs/api_server.log 2>&1 &
|
||||
API_PID=$!
|
||||
echo "API服务器已启动 (PID: $API_PID)"
|
||||
|
||||
# 等待API服务器启动
|
||||
sleep 3
|
||||
|
||||
# 检查API服务器是否正常启动
|
||||
if ! kill -0 $API_PID 2>/dev/null; then
|
||||
echo "错误: API服务器启动失败,请检查logs/api_server.log"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查HTTP端口
|
||||
if check_port $FRONTEND_PORT; then
|
||||
echo "警告: 端口$FRONTEND_PORT被占用,尝试清理..."
|
||||
pkill -f "python.*http.server.*$FRONTEND_PORT" 2>/dev/null || true
|
||||
sleep 2
|
||||
if check_port $FRONTEND_PORT; then
|
||||
echo "错误: 端口$FRONTEND_PORT仍被占用,请手动检查"
|
||||
cleanup
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 启动HTTP静态文件服务器
|
||||
echo "启动HTTP静态文件服务器 (端口$FRONTEND_PORT)..."
|
||||
python -m http.server $FRONTEND_PORT > logs/http_server.log 2>&1 &
|
||||
HTTP_PID=$!
|
||||
echo "HTTP服务器已启动 (PID: $HTTP_PID)"
|
||||
|
||||
# 等待HTTP服务器启动
|
||||
sleep 3
|
||||
|
||||
# 检查HTTP服务器是否正常启动
|
||||
if ! kill -0 $HTTP_PID 2>/dev/null; then
|
||||
echo "错误: HTTP服务器启动失败,请检查logs/http_server.log"
|
||||
cleanup
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "\n=== 服务器启动成功 ==="
|
||||
echo "虚拟环境: $VENV_DIR"
|
||||
echo "前端页面: http://localhost:$FRONTEND_PORT"
|
||||
echo "API接口: http://localhost:$BACKEND_PORT/system-info"
|
||||
echo "图片文件接口: http://localhost:$BACKEND_PORT/image-files"
|
||||
echo "日志文件: logs/api_server.log, logs/http_server.log"
|
||||
echo "\n按 Ctrl+C 停止服务器"
|
||||
|
||||
# 保持脚本运行,等待用户中断
|
||||
while true; do
|
||||
# 检查进程是否还在运行
|
||||
if ! kill -0 $API_PID 2>/dev/null; then
|
||||
echo "警告: API服务器进程已停止"
|
||||
break
|
||||
fi
|
||||
if ! kill -0 $HTTP_PID 2>/dev/null; then
|
||||
echo "警告: HTTP服务器进程已停止"
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
cleanup
|
||||
464
system_info.py
Normal file
464
system_info.py
Normal file
@@ -0,0 +1,464 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
跨平台系统信息采集脚本
|
||||
- 自动识别Windows/Linux(针对Debian内核6.12.18-trim做兼容)
|
||||
- 采集 CPU、内存、存储、网络等信息(不采集风扇、内存温度与硬盘温度)
|
||||
- 尽量使用标准库/psutil
|
||||
|
||||
运行:
|
||||
Windows: .venv\Scripts\python.exe system_info.py
|
||||
Linux : python3 system_info.py
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import psutil
|
||||
|
||||
# Windows 平台可选依赖
|
||||
try:
|
||||
import wmi # type: ignore
|
||||
except Exception:
|
||||
wmi = None # 非Windows或未安装时安全降级
|
||||
|
||||
|
||||
# ======================= 通用工具函数 =======================
|
||||
|
||||
def bytes_to_gb(b: float) -> float:
|
||||
"""将字节转换为GB并保留两位小数。"""
|
||||
return round(b / (1024 ** 3), 2)
|
||||
|
||||
|
||||
# ======================= CPU 信息 =======================
|
||||
|
||||
def get_cpu_usage() -> float:
|
||||
"""获取CPU当前占用率(百分比)。"""
|
||||
try:
|
||||
return float(psutil.cpu_percent(interval=1))
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
|
||||
def get_cpu_temperature() -> Optional[float]:
|
||||
"""获取CPU温度(摄氏度)。不同平台采用不同策略,获取失败返回None。"""
|
||||
system = platform.system()
|
||||
# Linux 优先使用 psutil.sensors_temperatures
|
||||
if system == 'Linux':
|
||||
try:
|
||||
temps = psutil.sensors_temperatures(fahrenheit=False) or {}
|
||||
# 常见键:coretemp/k10temp/zenpower
|
||||
for key in ('coretemp', 'k10temp', 'zenpower', 'cpu-thermal'):
|
||||
if key in temps and temps[key]:
|
||||
vals = [s.current for s in temps[key] if getattr(s, 'current', None) is not None]
|
||||
if vals:
|
||||
return float(max(vals))
|
||||
# 退化至 /sys/class/thermal
|
||||
base = '/sys/class/thermal'
|
||||
if os.path.isdir(base):
|
||||
res = []
|
||||
for name in os.listdir(base):
|
||||
if not name.startswith('thermal_zone'):
|
||||
continue
|
||||
tpath = os.path.join(base, name, 'temp')
|
||||
if os.path.isfile(tpath):
|
||||
try:
|
||||
val = int(open(tpath).read().strip())
|
||||
# 常见以毫度C表示
|
||||
if val > 1000:
|
||||
res.append(val / 1000.0)
|
||||
else:
|
||||
res.append(float(val))
|
||||
except Exception:
|
||||
pass
|
||||
if res:
|
||||
return float(max(res))
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
# Windows 尝试 WMI(MSAcpi_ThermalZoneTemperature),注意可能是主板而非核心温度
|
||||
if system == 'Windows' and wmi is not None:
|
||||
try:
|
||||
c = wmi.WMI(namespace='root\\WMI')
|
||||
sensors = c.MSAcpi_ThermalZoneTemperature()
|
||||
vals = []
|
||||
for s in sensors:
|
||||
# 温度单位为0.1K,需要转换摄氏度:C = (K/10) - 273.15
|
||||
if hasattr(s, 'CurrentTemperature'):
|
||||
celsius = s.CurrentTemperature / 10.0 - 273.15
|
||||
vals.append(celsius)
|
||||
if vals:
|
||||
return float(max(vals))
|
||||
except Exception:
|
||||
pass
|
||||
# 尝试 OpenHardwareMonitor WMI(如果用户已运行该工具)
|
||||
try:
|
||||
c = wmi.WMI(namespace='root\\OpenHardwareMonitor')
|
||||
for sensor in getattr(c, 'Sensor', lambda: [])():
|
||||
if getattr(sensor, 'SensorType', '') == 'Temperature' and 'cpu' in sensor.Name.lower():
|
||||
return float(sensor.Value)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ======================= 内存信息 =======================
|
||||
|
||||
def get_memory_usage() -> Dict[str, float]:
|
||||
"""获取内存使用信息(已用/总量,GB)。"""
|
||||
try:
|
||||
vm = psutil.virtual_memory()
|
||||
return {
|
||||
'used_gb': bytes_to_gb(vm.total - vm.available),
|
||||
'total_gb': bytes_to_gb(vm.total),
|
||||
}
|
||||
except Exception:
|
||||
return {'used_gb': 0.0, 'total_gb': 0.0}
|
||||
|
||||
# 已删除 get_memory_temperature(不再采集内存温度)
|
||||
|
||||
|
||||
# ======================= 存储信息 =======================
|
||||
|
||||
def _linux_list_block_devices() -> List[str]:
|
||||
"""Linux 获取物理块设备列表(如 sda、nvme0n1、vda)。"""
|
||||
res = []
|
||||
base = '/sys/block'
|
||||
if os.path.isdir(base):
|
||||
for name in os.listdir(base):
|
||||
if re.match(r'^(sd[a-z]+|nvme\d+n\d+|vd[a-z]+)$', name):
|
||||
res.append(name)
|
||||
return res
|
||||
|
||||
|
||||
def _linux_disk_size_bytes(dev: str) -> Optional[int]:
|
||||
"""Linux 通过 /sys/block/<dev>/size 读取总字节数。"""
|
||||
p = f'/sys/block/{dev}/size'
|
||||
try:
|
||||
sectors = int(open(p).read().strip())
|
||||
return sectors * 512
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _linux_disk_partitions_map() -> Dict[str, List[str]]:
|
||||
"""构建物理盘到分区设备路径的映射,例如 {'sda': ['/dev/sda1', ...]}。"""
|
||||
mapping: Dict[str, List[str]] = {}
|
||||
for part in psutil.disk_partitions(all=False):
|
||||
dev = part.device # 如 /dev/sda1
|
||||
mountpoint = part.mountpoint # 挂载点
|
||||
|
||||
# 处理标准分区格式 /dev/sda1
|
||||
m = re.match(r'/dev/([a-z]+)\d+$', dev)
|
||||
if not m:
|
||||
# 处理 nvme0n1p1
|
||||
m = re.match(r'/dev/(nvme\d+n\d+)p\d+$', dev)
|
||||
if not m:
|
||||
# 处理整个磁盘作为分区的情况 /dev/sda
|
||||
m = re.match(r'/dev/([a-z]+)$', dev)
|
||||
if not m:
|
||||
# 处理 nvme0n1 整盘
|
||||
m = re.match(r'/dev/(nvme\d+n\d+)$', dev)
|
||||
|
||||
if m:
|
||||
root = m.group(1)
|
||||
mapping.setdefault(root, []).append(mountpoint) # 使用挂载点而不是设备路径
|
||||
return mapping
|
||||
|
||||
# 已删除 _linux_disk_temperature(不再采集硬盘温度)
|
||||
|
||||
|
||||
def get_storage_info() -> Dict[str, Any]:
|
||||
"""获取存储信息:硬盘数量、每个硬盘的容量(已用/总、GB)。"""
|
||||
system = platform.system()
|
||||
result: Dict[str, Any] = {
|
||||
'disk_count': 0,
|
||||
'disks': [],
|
||||
}
|
||||
|
||||
if system == 'Linux':
|
||||
# 直接遍历已挂载的分区,而不是物理硬盘设备
|
||||
processed_mountpoints = set()
|
||||
|
||||
for part in psutil.disk_partitions(all=False):
|
||||
mountpoint = part.mountpoint
|
||||
device = part.device
|
||||
|
||||
# 跳过已处理的挂载点
|
||||
if mountpoint in processed_mountpoints:
|
||||
continue
|
||||
|
||||
# 跳过特殊文件系统
|
||||
if part.fstype in ['tmpfs', 'devtmpfs', 'sysfs', 'proc', 'devpts']:
|
||||
continue
|
||||
|
||||
try:
|
||||
usage = psutil.disk_usage(mountpoint)
|
||||
|
||||
# 为不同挂载点设置合适的显示名称
|
||||
if mountpoint == '/':
|
||||
display_name = '系统盘 (/)'
|
||||
elif mountpoint == '/vol1':
|
||||
display_name = '/vol1 (应用存储)'
|
||||
elif mountpoint.startswith('/vol'):
|
||||
display_name = f'{mountpoint} (存储卷)'
|
||||
else:
|
||||
display_name = f'{mountpoint} ({device})'
|
||||
|
||||
result['disks'].append({
|
||||
'name': display_name,
|
||||
'used_gb': bytes_to_gb(usage.used),
|
||||
'total_gb': bytes_to_gb(usage.total),
|
||||
})
|
||||
|
||||
processed_mountpoints.add(mountpoint)
|
||||
|
||||
except Exception as e:
|
||||
# print(f"获取 {mountpoint} 使用量失败: {e}")
|
||||
pass
|
||||
result['disk_count'] = len(result['disks'])
|
||||
return result
|
||||
|
||||
if system == 'Windows' and wmi is not None:
|
||||
try:
|
||||
c = wmi.WMI()
|
||||
# 通过关联链路汇总每个物理盘上的分区占用
|
||||
for disk in c.Win32_DiskDrive():
|
||||
total_b = int(getattr(disk, 'Size', 0) or 0)
|
||||
used_b = 0
|
||||
try:
|
||||
for part in disk.associators("Win32_DiskDriveToDiskPartition"):
|
||||
for ld in part.associators("Win32_LogicalDiskToPartition"):
|
||||
mount = ld.DeviceID + "\\" # 如 C:\
|
||||
try:
|
||||
u = psutil.disk_usage(mount)
|
||||
used_b += u.used
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
result['disks'].append({
|
||||
'name': f"{disk.Model}",
|
||||
'used_gb': bytes_to_gb(used_b),
|
||||
'total_gb': bytes_to_gb(total_b),
|
||||
})
|
||||
result['disk_count'] = len(result['disks'])
|
||||
return result
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 兜底:按挂载点统计(不区分物理盘)
|
||||
mounts = []
|
||||
seen = set()
|
||||
for p in psutil.disk_partitions(all=False):
|
||||
if p.mountpoint in seen:
|
||||
continue
|
||||
seen.add(p.mountpoint)
|
||||
try:
|
||||
u = psutil.disk_usage(p.mountpoint)
|
||||
mounts.append({
|
||||
'name': p.device,
|
||||
'used_gb': bytes_to_gb(u.used),
|
||||
'total_gb': bytes_to_gb(u.total),
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
result['disks'] = mounts
|
||||
result['disk_count'] = len(mounts)
|
||||
return result
|
||||
|
||||
|
||||
# ======================= 网络信息 =======================
|
||||
|
||||
def get_ip_interfaces() -> List[Tuple[str, str]]:
|
||||
"""获取所有具有IPv4地址的网卡及其IP。屏蔽以 br- 开头的 bridge 网卡(如 Docker 的 br-f86755ba2795)。"""
|
||||
import socket
|
||||
res: List[Tuple[str, str]] = []
|
||||
addrs = psutil.net_if_addrs()
|
||||
AF_LINK = getattr(psutil, 'AF_LINK', None)
|
||||
for name, arr in addrs.items():
|
||||
# 屏蔽 bridge 网卡(如 Docker 的 br-xxxxxxxxxxxx)
|
||||
if name.startswith('br-'):
|
||||
continue
|
||||
for a in arr:
|
||||
fam = getattr(a, 'family', None)
|
||||
if AF_LINK is not None and fam == AF_LINK:
|
||||
continue
|
||||
if fam == socket.AF_INET:
|
||||
res.append((name, a.address))
|
||||
return res
|
||||
|
||||
|
||||
def get_network_rate(sample_seconds: float = 1.0) -> Dict[str, float]:
|
||||
"""采样一段时间,估算总上行/下行速率(Mbps)。"""
|
||||
try:
|
||||
c1 = psutil.net_io_counters()
|
||||
time.sleep(sample_seconds)
|
||||
c2 = psutil.net_io_counters()
|
||||
up_bps = (c2.bytes_sent - c1.bytes_sent) * 8.0 / sample_seconds
|
||||
down_bps = (c2.bytes_recv - c1.bytes_recv) * 8.0 / sample_seconds
|
||||
return {
|
||||
'up_mbps': round(up_bps / 1_000_000, 3),
|
||||
'down_mbps': round(down_bps / 1_000_000, 3),
|
||||
}
|
||||
except Exception:
|
||||
return {'up_mbps': 0.0, 'down_mbps': 0.0}
|
||||
|
||||
# 已删除风扇信息相关代码(get_fans_info)
|
||||
|
||||
|
||||
# ======================= 主流程 =======================
|
||||
|
||||
def is_debian_kernel_special() -> bool:
|
||||
"""是否为需要特别兼容的Debian内核版本 6.12.18-trim。"""
|
||||
try:
|
||||
return platform.system() == 'Linux' and platform.release().strip() == '6.12.18-trim'
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def collect_system_info() -> Dict[str, Any]:
|
||||
"""汇总采集所有信息,返回结构化字典。"""
|
||||
info: Dict[str, Any] = {
|
||||
'os': platform.system(),
|
||||
'os_release': platform.release(),
|
||||
'os_version': platform.version(),
|
||||
'debian_kernel_6_12_18_trim': is_debian_kernel_special(),
|
||||
}
|
||||
|
||||
# CPU
|
||||
info['cpu'] = {
|
||||
'usage_percent': get_cpu_usage(),
|
||||
'temperature_c': get_cpu_temperature(),
|
||||
}
|
||||
|
||||
# Memory
|
||||
mem = get_memory_usage()
|
||||
info['memory'] = mem
|
||||
|
||||
# Storage
|
||||
info['storage'] = get_storage_info()
|
||||
|
||||
# Network
|
||||
info['network'] = {
|
||||
'interfaces': [{'name': n, 'ip': ip} for n, ip in get_ip_interfaces()],
|
||||
**get_network_rate(1.0),
|
||||
}
|
||||
|
||||
return info
|
||||
|
||||
|
||||
# 新增:FastAPI 与 CORS、Uvicorn(仅在 --serve 模式下使用)
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import uvicorn
|
||||
|
||||
|
||||
# ======================= 图片文件扫描 =======================
|
||||
|
||||
def get_image_files() -> Dict[str, List[str]]:
|
||||
"""扫描image目录中的所有图片文件
|
||||
|
||||
Returns:
|
||||
Dict: 包含图片文件列表的字典
|
||||
"""
|
||||
try:
|
||||
# 获取当前脚本所在目录
|
||||
script_dir = Path(__file__).parent.absolute()
|
||||
image_dir = script_dir / "image"
|
||||
|
||||
# 支持的图片格式
|
||||
image_extensions = {'.gif', '.jpg', '.jpeg', '.png', '.bmp', '.webp'}
|
||||
|
||||
image_files = []
|
||||
|
||||
if image_dir.exists() and image_dir.is_dir():
|
||||
for file_path in image_dir.iterdir():
|
||||
if file_path.is_file() and file_path.suffix.lower() in image_extensions:
|
||||
image_files.append(file_path.name)
|
||||
|
||||
# 按文件名排序
|
||||
image_files.sort()
|
||||
|
||||
return {
|
||||
"files": image_files,
|
||||
"count": len(image_files),
|
||||
"directory": str(image_dir)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"files": [],
|
||||
"count": 0,
|
||||
"error": str(e),
|
||||
"directory": ""
|
||||
}
|
||||
|
||||
|
||||
# ======================= HTTP 服务(FastAPI) =======================
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""创建 FastAPI 应用并注册路由。
|
||||
- 提供 GET /system-info 端点,返回 collect_system_info() 的结果
|
||||
- 默认开启 CORS 以便前后端跨域访问(生产可按需收敛域名)
|
||||
"""
|
||||
app = FastAPI(title="NAS Monitor Python Backend")
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.get("/system-info")
|
||||
def system_info_endpoint():
|
||||
return collect_system_info()
|
||||
|
||||
@app.get("/image-files")
|
||||
def image_files_endpoint():
|
||||
"""获取image目录中的所有图片文件列表"""
|
||||
return get_image_files()
|
||||
|
||||
return app
|
||||
|
||||
|
||||
def serve_api(host: str = "0.0.0.0", port: int = 8000) -> None:
|
||||
"""启动 Uvicorn 服务,运行 FastAPI 应用。"""
|
||||
uvicorn.run(create_app(), host=host, port=port, log_level="info")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""主函数:采集信息并以JSON形式输出(CLI 模式)。"""
|
||||
data = collect_system_info()
|
||||
print(json.dumps(data, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
# 在 Windows 下确保 stdout 使用 UTF-8,避免中文网卡名乱码
|
||||
import sys
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 新增:解析命令行参数,支持 --serve 模式
|
||||
parser = argparse.ArgumentParser(description="NAS Monitor system_info 服务/脚本")
|
||||
parser.add_argument("--serve", action="store_true", help="以 HTTP API 模式提供 /system-info")
|
||||
parser.add_argument("--host", default=os.environ.get("PY_HOST", "0.0.0.0"), help="HTTP 监听地址,默认 0.0.0.0")
|
||||
parser.add_argument("--port", type=int, default=int(os.environ.get("PY_PORT", "8000")), help="HTTP 监听端口,默认 8000")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.serve:
|
||||
serve_api(args.host, args.port)
|
||||
else:
|
||||
main()
|
||||
164
项目任务目标.md
Normal file
164
项目任务目标.md
Normal file
@@ -0,0 +1,164 @@
|
||||
# NAS监控系统项目任务目标
|
||||
|
||||
## 当前问题修复任务
|
||||
|
||||
### 高优先级任务
|
||||
- [x] 创建项目任务目标文件,跟踪修复进度
|
||||
- [x] 修复start_server.sh中的Bad substitution错误(第6行语法问题)
|
||||
- [x] 创建适用于虚拟环境的启动脚本版本
|
||||
|
||||
### 中优先级任务
|
||||
- [ ] 测试修复后的脚本在Linux环境下的运行
|
||||
- [x] 优化Python依赖管理,支持虚拟环境
|
||||
|
||||
### 低优先级任务
|
||||
- [ ] 添加更详细的错误处理和日志记录
|
||||
- [ ] 创建Windows批处理脚本版本
|
||||
|
||||
## 问题分析
|
||||
|
||||
### 当前遇到的问题:
|
||||
1. **Bad substitution错误**:shell脚本第6行存在语法问题
|
||||
2. **Python环境问题**:需要支持虚拟环境运行
|
||||
3. **跨平台兼容性**:需要同时支持Linux和Windows
|
||||
|
||||
### 解决方案:
|
||||
1. 修复shell脚本语法错误
|
||||
2. 创建支持虚拟环境的启动脚本
|
||||
3. 提供多种启动方式的选择
|
||||
|
||||
## 项目结构
|
||||
```
|
||||
nasweb/
|
||||
├── README.md
|
||||
├── index.html # 前端页面
|
||||
├── system_info.py # 系统信息API服务
|
||||
├── start_server.py # Python启动脚本(原版)
|
||||
├── start_server.sh # Linux shell启动脚本(已修复)
|
||||
├── start_server_venv.py # Python启动脚本(虚拟环境版本)
|
||||
├── start_server_venv.sh # Linux shell启动脚本(虚拟环境版本)
|
||||
├── requirements.txt # Python依赖包列表
|
||||
├── 项目任务目标.md # 本文件
|
||||
├── image/ # 图片资源
|
||||
├── logs/ # 日志文件
|
||||
└── venv/ # 虚拟环境目录(运行时创建)
|
||||
```
|
||||
|
||||
## 更新日志
|
||||
- 2024-01-XX: 创建任务目标文件
|
||||
- 2024-01-XX: 修复start_server.sh中的Bad substitution错误
|
||||
- 2024-01-XX: 创建虚拟环境版本的启动脚本
|
||||
- 2024-01-XX: 添加requirements.txt依赖管理文件
|
||||
- 2024-01-XX: 修复Python检测逻辑,解决Linux环境下python3命令检测问题
|
||||
|
||||
## 最新修复 (2025-01-09)
|
||||
|
||||
### 修复的问题:
|
||||
1. ✅ Python检测问题:系统中python3存在但`command -v python3`检测失败
|
||||
2. ✅ Shell兼容性问题:Linux环境下用sh执行脚本时source命令不可用
|
||||
3. ✅ 硬盘使用量检测问题:Linux环境下硬盘使用量显示为0
|
||||
4. ✅ 进程终止问题:Ctrl+C退出后HTTP端口未正确关闭
|
||||
5. ✅ 端口配置问题:需要修改API和HTTP服务端口
|
||||
6. ✅ 硬盘检测范围问题:需要检测/vol1应用存储目录
|
||||
7. ✅ 硬盘容量显示错误:显示物理硬盘总容量而非分区容量
|
||||
8. ✅ 信号处理语法错误:修复trap命令在某些shell环境下的语法问题
|
||||
9. ✅ 端口占用问题:添加端口检查和自动清理机制
|
||||
10. ✅ 系统服务部署:创建systemd service配置实现开机自启和后台运行
|
||||
11. ✅ systemd配置错误修复:修复ExecStart重复字段和sudo使用问题
|
||||
12. ✅ 服务用户配置优化:改为使用root用户运行,避免用户组权限问题
|
||||
13. ✅ GIF文件显示修复:更新文件列表匹配实际存在的GIF文件,解决图片显示问题
|
||||
14. ✅ 动态图片扫描:实现启动时自动扫描image目录,动态获取图片文件列表
|
||||
15. ✅ 端口配置文件:实现从config.json读取端口配置,支持动态端口设置
|
||||
|
||||
### 改进内容:
|
||||
|
||||
#### 1. Python检测修复
|
||||
- 改进Python检测逻辑:使用`which python3`和`python3 --version`双重检测
|
||||
- 修复start_server_venv.sh和start_server.sh中的Python检测问题
|
||||
- 增强用户体验:添加Python版本显示和安装建议
|
||||
|
||||
#### 2. Shell兼容性修复
|
||||
- 将`source`命令替换为`.`命令,提高shell兼容性
|
||||
- 在cleanup函数中添加deactivate命令的存在性检查
|
||||
|
||||
#### 3. 硬盘使用量检测修复
|
||||
- 扩展正则匹配:支持整盘分区格式(如sda)和nvme设备格式(如nvme0n1)
|
||||
- 改进挂载点映射:将设备路径映射改为挂载点映射
|
||||
- 添加根分区处理:当part_map中没有对应挂载点时,检查根分区
|
||||
- 增强错误处理:添加异常捕获和调试信息
|
||||
- 创建测试工具:test_disk_usage.py用于验证硬盘检测功能
|
||||
|
||||
#### 4. 进程管理和端口配置修复
|
||||
- 改进cleanup函数:添加强制终止逻辑(kill -9)和残留进程清理
|
||||
- 修改端口配置:API端口改为8002,HTTP端口改为8003
|
||||
- 更新前端配置:修改index.html中的API端口引用
|
||||
- 同步两个启动脚本的端口配置
|
||||
|
||||
#### 5. 存储检测增强
|
||||
- 添加/vol1目录检测:专门检测应用存储目录的使用情况
|
||||
- 支持独立挂载点:将/vol1作为独立磁盘显示
|
||||
- 改进存储信息展示:区分系统盘和应用存储盘
|
||||
- 修复容量显示错误:改为显示分区实际容量而非物理硬盘总容量
|
||||
- 优化分区识别:直接遍历挂载分区,过滤特殊文件系统
|
||||
- 改进显示名称:为不同类型的挂载点设置合适的显示名称
|
||||
|
||||
#### 6. 用户体验改进
|
||||
- 添加详细的错误提示和解决建议
|
||||
- 改进日志输出格式
|
||||
- 增强脚本的健壮性和兼容性
|
||||
|
||||
#### 7. 信号处理优化
|
||||
- 修复trap命令语法兼容性问题
|
||||
- 改进SIGINT/SIGTERM信号处理
|
||||
- 确保在不同shell环境下正常工作
|
||||
|
||||
#### 8. 端口管理增强
|
||||
- 添加端口占用检查机制
|
||||
- 实现自动进程清理功能
|
||||
- 防止端口冲突导致的启动失败
|
||||
|
||||
#### 9. 系统服务集成
|
||||
- 创建systemd service配置文件
|
||||
- 实现开机自启动功能
|
||||
- 提供服务管理脚本
|
||||
- 支持后台运行和自动重启
|
||||
- 完整的部署和故障排除文档
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 方式1:使用原版启动脚本
|
||||
```bash
|
||||
# Linux
|
||||
sudo sh ./start_server.sh
|
||||
|
||||
# Windows/Linux
|
||||
python start_server.py
|
||||
```
|
||||
|
||||
### 方式2:使用虚拟环境版本(推荐)
|
||||
```bash
|
||||
# Linux
|
||||
sh ./start_server_venv.sh
|
||||
|
||||
# Windows/Linux
|
||||
python start_server_venv.py
|
||||
```
|
||||
|
||||
### 方式3:手动使用虚拟环境
|
||||
```bash
|
||||
# 创建虚拟环境
|
||||
python3 -m venv venv
|
||||
|
||||
# 激活虚拟环境
|
||||
# Linux/macOS:
|
||||
source venv/bin/activate
|
||||
# Windows:
|
||||
venv\Scripts\activate
|
||||
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 运行服务
|
||||
python system_info.py --serve --port 8001 &
|
||||
python -m http.server 8000
|
||||
```
|
||||
Reference in New Issue
Block a user