<think>由于代码量较大,我将分别提供优化后的`detail_page.py`和`server_manager.py`代码。优化点包括:
1. 使用上下文管理器确保SSH连接安全
2. 增加文件校验(MD5)
3. 使用线程池管理上传任务
4. 添加自定义异常类
5. 增强日志记录
注意:由于代码较长,我将分两个部分提供。
首先,优化后的`detail_page.py`:</think>### 优化后的 `detail_page.py`
```python
import sys
import os
import time
import json
import subprocess
import psutil
import logging
from datetime import datetime
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QProgressBar, QTableWidget, QTableWidgetItem, QHeaderView,
QTextEdit, QPushButton, QSplitter, QListWidget, QGroupBox, QScrollArea,
QMessageBox, QComboBox, QAbstractItemView, QSizePolicy, QFileDialog, QProgressDialog
)
from PyQt5.QtCore import Qt, QTimer, QThread, pyqtSignal, QRunnable, QThreadPool, QObject
from PyQt5.QtGui import QFont, QColor, QBrush, QIcon, QPainter
import ssh_manger
import hashlib
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('DetailPage')
REPORT_FILE = "/home/mss_tool/infos/system_report.json" # 报告文件名
class UploadSignals(QObject):
"""上传任务信号"""
progress_updated = pyqtSignal(int)
finished = pyqtSignal(bool, str)
log_message = pyqtSignal(str)
class UploadTask(QRunnable):
"""JAR上传线程任务"""
def __init__(self, server_info, remote_path, local_path, ssh_manager):
super().__init__()
self.server_info = server_info
self.remote_path = remote_path
self.local_path = local_path
self.ssh_manager = ssh_manager
self.signals = UploadSignals()
self.cancel_flag = False
self.setAutoDelete(True)
def run(self):
"""执行上传操作"""
try:
self.signals.log_message.emit("开始上传JAR文件")
# 计算本地文件MD5
local_hash = self._calculate_md5(self.local_path)
self.signals.log_message.emit(f"本地文件MD5: {local_hash}")
# 1. 停止旧服务
jar_name = os.path.basename(self.remote_path)
jar_dir = os.path.dirname(self.remote_path)
# 获取PID并停止服务
get_pid_cmd = (
f"ps aux | grep '{jar_name}' | grep '{jar_dir}' | grep -v grep | "
f"awk '{{print $2}}'"
)
self.signals.log_message.emit(f"停止旧服务: {get_pid_cmd}")
success, exit_code, stdout, stderr = self.ssh_manager.execute_command(
get_pid_cmd, sudo=True
)
pids = []
if success and stdout.strip():
pids = stdout.split()
for pid in pids:
kill_cmd = f"kill -9 {pid}"
self.signals.log_message.emit(f"停止进程: {kill_cmd}")
self.ssh_manager.execute_command(kill_cmd, sudo=True)
time.sleep(2)
self.signals.progress_updated.emit(10)
self.signals.log_message.emit("旧服务已停止")
# 2. 备份旧JAR文件
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
backup_name = f"{jar_name}.bak_{timestamp}"
backup_path = os.path.join(jar_dir, backup_name)
# 重命名命令
rename_cmd = f"mv '{self.remote_path}' '{backup_path}'"
self.signals.log_message.emit(f"备份旧文件: {rename_cmd}")
success, exit_code, stdout, stderr = self.ssh_manager.execute_command(
rename_cmd, sudo=True
)
if not success:
error_msg = f"无法备份旧JAR文件: {stderr}\n命令: {rename_cmd}"
self.signals.log_message.emit(error_msg)
self.signals.finished.emit(False, error_msg)
return
self.signals.progress_updated.emit(20)
self.signals.log_message.emit("备份成功")
# 3. 上传新JAR文件
self.signals.log_message.emit(f"开始上传新JAR文件: {self.local_path} -> {self.remote_path}")
success, message = self.ssh_manager.upload_file(
self.local_path,
self.remote_path,
overwrite=True,
progress_callback=self.update_progress
)
if self.cancel_flag:
self.signals.log_message.emit("上传已取消")
self.signals.finished.emit(False, "上传已取消")
return
if not success:
# 尝试恢复备份
self.signals.log_message.emit(f"上传失败,尝试恢复备份: {backup_path}")
restore_cmd = f"mv '{backup_path}' '{self.remote_path}'"
self.ssh_manager.execute_command(restore_cmd, sudo=True)
self.signals.finished.emit(False, f"上传JAR文件失败: {message}")
return
self.signals.progress_updated.emit(70)
self.signals.log_message.emit("JAR文件上传成功")
# 4. 校验文件MD5
self.signals.log_message.emit("开始校验文件MD5")
remote_hash = self._get_remote_md5(self.remote_path)
self.signals.log_message.emit(f"远程文件MD5: {remote_hash}")
if remote_hash != local_hash:
error_msg = f"MD5校验失败: 本地({local_hash}) != 远程({remote_hash})"
self.signals.log_message.emit(error_msg)
# 恢复备份
self.ssh_manager.execute_command(f"mv '{backup_path}' '{self.remote_path}'", sudo=True)
self.signals.finished.emit(False, error_msg)
return
self.signals.progress_updated.emit(75)
self.signals.log_message.emit("MD5校验成功")
# 5. 启动新服务
start_cmd = (
f"cd '{jar_dir}' && "
f"nohup java -jar '{self.remote_path}' > '{self.remote_path}.log' 2>&1 &"
)
self.signals.log_message.emit(f"启动新服务: {start_cmd}")
success, exit_code, stdout, stderr = self.ssh_manager.execute_command(
start_cmd, sudo=True
)
if not success:
error_msg = f"启动服务失败: {stderr}\n启动命令: {start_cmd}"
self.signals.log_message.emit(error_msg)
self.signals.finished.emit(False, error_msg)
return
# 6. 检查服务是否启动成功
self.signals.progress_updated.emit(80)
self.signals.log_message.emit("检查服务状态...")
time.sleep(5) # 等待服务启动
# 检查进程是否存在
check_pid_cmd = (
f"ps aux | grep '{jar_name}' | grep '{jar_dir}' | grep -v grep | "
f"awk '{{print $2}}'"
)
success, exit_code, stdout, stderr = self.ssh_manager.execute_command(
check_pid_cmd, sudo=True
)
if not success or not stdout.strip():
# 尝试读取日志
log_file = f"{self.remote_path}.log"
success, log_content = self.ssh_manager.read_file(log_file, retries=1)
if success:
# 截取日志末尾
if len(log_content) > 1000:
log_snippet = log_content[-1000:]
else:
log_snippet = log_content
error_msg = f"服务启动失败,日志末尾内容:\n{log_snippet}"
else:
error_msg = "服务启动失败,且无法读取日志文件"
self.signals.log_message.emit(error_msg)
self.signals.finished.emit(False, error_msg)
return
self.signals.progress_updated.emit(95)
self.signals.log_message.emit("服务启动成功")
# 7. 重启监控服务
self.signals.log_message.emit("重启监控服务...")
restart_cmd = f"cd /home/mss_tool && ./monitor restart"
success, exit_code, stdout, stderr = self.ssh_manager.execute_command(
restart_cmd, sudo=True
)
if not success:
self.signals.log_message.emit(f"重启监控失败: {stderr}")
else:
self.signals.log_message.emit("监控服务已重启")
self.signals.progress_updated.emit(100)
result_msg = (
f"JAR文件上传并启动成功!\n"
f"备份路径: {backup_path}\n"
f"新文件: {self.remote_path}\n"
f"MD5: {local_hash}\n"
f"启动命令: {start_cmd}\n"
f"监控已重启"
)
self.signals.log_message.emit(result_msg)
self.signals.finished.emit(True, result_msg)
except Exception as e:
error_msg = f"操作过程中出错: {str(e)}"
self.signals.log_message.emit(error_msg)
self.signals.finished.emit(False, error_msg)
def update_progress(self, percent):
"""更新进度"""
self.signals.progress_updated.emit(percent)
self.signals.log_message.emit(f"上传进度: {percent:.1f}%")
def cancel_upload(self):
"""取消上传操作"""
self.cancel_flag = True
if self.ssh_manager:
try:
self.ssh_manager.set_cancel_flag(True)
except:
pass
def _calculate_md5(self, file_path):
"""计算文件MD5"""
hash_md5 = hashlib.md5()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_md5.update(chunk)
return hash_md5.hexdigest()
def _get_remote_md5(self, remote_path):
"""获取远程文件MD5"""
cmd = f"md5sum {remote_path} | awk '{{print $1}}'"
success, exit_code, stdout, stderr = self.ssh_manager.execute_command(cmd)
if not success:
raise Exception(f"获取远程MD5失败: {stderr}")
return stdout.strip()
# 其他类保持不变(StatusDot, DistUploadThread等)
class SystemMonitorUI(QMainWindow):
def __init__(self, info, ssh_manager):
super().__init__()
# ... 初始化代码保持不变 ...
# 创建线程池
self.thread_pool = QThreadPool.globalInstance()
self.thread_pool.setMaxThreadCount(3) # 限制同时上传任务数量
def upload_jar(self):
"""使用线程池优化JAR上传"""
button = self.sender()
jar_name = button.property("jar_name")
jar_path = button.property("jar_path")
server_info = button.property("server_info")
if not jar_path or jar_path == "0":
QMessageBox.warning(self, "警告", "该JAR服务未配置路径!")
return
# 弹出文件对话框选择JAR文件
file_path, _ = QFileDialog.getOpenFileName(
self, "选择JAR文件", "", "JAR Files (*.jar);;All Files (*)"
)
if not file_path:
return
# 创建进度对话框
progress_dialog = QProgressDialog("正在上传JAR文件...", "取消", 0, 100, self)
progress_dialog.setWindowTitle("上传JAR文件")
progress_dialog.setWindowModality(Qt.WindowModal)
progress_dialog.setAutoClose(True)
progress_dialog.setAutoReset(True)
progress_dialog.setMinimumDuration(0)
progress_dialog.setValue(0)
# 添加日志显示区域
log_text = QTextEdit()
log_text.setReadOnly(True)
log_text.setMinimumHeight(100)
layout = progress_dialog.layout()
if isinstance(layout, QVBoxLayout):
layout.addWidget(log_text)
# 创建上传任务
upload_task = UploadTask(server_info, jar_path, file_path, self.ssh_manager)
# 连接信号
upload_task.signals.progress_updated.connect(progress_dialog.setValue)
upload_task.signals.finished.connect(
lambda success, message: self.handle_upload_result(success, message, progress_dialog)
)
upload_task.signals.log_message.connect(log_text.append)
# 提交任务到线程池
self.thread_pool.start(upload_task)
# 连接取消按钮
progress_dialog.canceled.connect(upload_task.cancel_upload)
# 其他方法保持不变...
```
### 优化后的 `server_manager.py`
```python
import sys
import json
import os
import base64
import hashlib
import logging
import shutil
import threading
from threading import Thread, Event
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton,
QTableWidget, QTableWidgetItem, QDialog, QLineEdit, QLabel, QFormLayout,
QMessageBox, QHeaderView, QInputDialog, QGridLayout, QGroupBox, QScrollArea,
QProgressDialog, QTabWidget, QTabBar
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QFont, QIcon, QFontMetrics
import time
import detail_page
import ssh_manger
# 配置日志
logging.basicConfig(
filename='server_manager.log',
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def resource_path(relative_path):
"""获取资源的绝对路径"""
if hasattr(sys, '_MEIPASS'):
return os.path.join(sys._MEIPASS, relative_path)
return os.path.join(os.path.abspath("."), relative_path)
# 配置常量
LOCAL_SCRIPT = "monitor"
REMOTE_DIR = "/home/mss_tool/"
REMOTE_VERSION = os.path.join(REMOTE_DIR, "version.txt")
VERSION = '1.0.3'
REMOTE_SCRIPT = os.path.join(REMOTE_DIR, os.path.basename(LOCAL_SCRIPT))
# 通知服务常量
ALERT_VERSION = '1.0.3'
ALERT_SCRIPT = "mss_alert"
REMOTE_ALERT_SCRIPT = os.path.join(REMOTE_DIR, ALERT_SCRIPT)
# 加密解密工具类
class EncryptionManager:
def __init__(self, key=None):
self.key = key if key else "Mko09ijn@" # 默认密钥
def encrypt(self, data):
"""简单加密方法"""
if isinstance(data, str):
data = data.encode('utf-8')
# 使用密钥生成哈希作为加密密钥
key_hash = hashlib.sha256(self.key.encode()).digest()
# 简单的XOR加密
encrypted = bytearray()
for i in range(len(data)):
encrypted.append(data[i] ^ key_hash[i % len(key_hash)])
return base64.b64encode(encrypted).decode('utf-8')
def decrypt(self, enc_data):
"""简单解密方法"""
enc_data = base64.b64decode(enc_data)
# 使用密钥生成哈希作为加密密钥
key_hash = hashlib.sha256(self.key.encode()).digest()
# 简单的XOR解密
decrypted = bytearray()
for i in range(len(enc_data)):
decrypted.append(enc_data[i] ^ key_hash[i % len(key_hash)])
return decrypted.decode('utf-8')
# 添加/编辑服务器对话框(保持不变)
# 通知服务器配置对话框(保持不变)
# SSH连接和部署线程(用于通知服务)(保持不变)
# SSH连接和部署线程(保持不变)
# 主窗口
class ServerManager(QMainWindow):
def __init__(self):
super().__init__()
self.current_password = "Mko09ijn@"
self.enc_manager = EncryptionManager(self.current_password)
self.servers = []
self.ssh_connection_pool = {} # SSH连接池
self.init_ui()
self.load_servers()
def get_ssh_connection(self, server_info):
"""从连接池获取SSH连接"""
key = f"{server_info['ip']}:{server_info.get('port', 22)}"
# 检查连接是否有效
if key in self.ssh_connection_pool:
ssh_manager = self.ssh_connection_pool[key]
if ssh_manager.ssh and ssh_manager.ssh.get_transport() and ssh_manager.ssh.get_transport().is_active():
return ssh_manager
else:
# 连接无效,创建新连接
logger.info(f"连接池中的连接无效,创建新连接: {key}")
del self.ssh_connection_pool[key]
# 创建新连接
logger.info(f"创建新SSH连接: {key}")
ssh_manager = ssh_manger.SSHManager(server_info)
success, message = ssh_manager.connect()
if success:
ssh_manager.start_heartbeat()
self.ssh_connection_pool[key] = ssh_manager
return ssh_manager
else:
raise Exception(f"无法创建SSH连接: {message}")
def open_ssh(self, row):
"""打开SSH监控窗口(使用连接池优化)"""
server_info = self.servers[row]
server_ip = server_info['ip']
jump_ip = server_info.get('jump_ip', '')
remark = server_info.get('remark', '')
# 检查是否已打开该服务器的监控
for i in range(1, self.tab_widget.count()):
if self.tab_info.get(i, {}).get('ip') == server_ip:
self.tab_widget.setCurrentIndex(i)
self.ensure_tab_visible(i)
return
# 创建进度对话框
progress = QProgressDialog(f"正在连接服务器 {server_ip}...", "取消", 0, 0, self)
progress.setWindowModality(Qt.WindowModal)
progress.setCancelButton(None)
progress.show()
try:
# 从连接池获取SSH连接
ssh_manager = self.get_ssh_connection(server_info)
# 创建监控窗口
monitor_window = detail_page.SystemMonitorUI(server_info, ssh_manager)
# 添加为标签页
tab_index = self.tab_widget.addTab(monitor_window, self.generate_tab_name(server_info))
# 存储标签页信息
self.tab_info[tab_index] = {
'ip': server_info['ip'],
'jump_ip': server_info.get('jump_ip', ''),
'remark': server_info.get('remark', ''),
'ssh_manager': ssh_manager,
'monitor_window': monitor_window
}
# 切换到新标签页
self.tab_widget.setCurrentIndex(tab_index)
self.ensure_tab_visible(tab_index)
logger.info(f"成功打开监控窗口: {server_info['ip']}")
progress.close()
except Exception as e:
progress.close()
QMessageBox.critical(self, "连接失败", f"连接失败: {str(e)}")
logger.error(f"打开监控窗口失败: {str(e)}")
def close_tab(self, index):
"""关闭标签页(优化连接管理)"""
if index == 0: # 防止关闭服务器管理标签
return
# 关闭SSH连接(但不从连接池移除)
tab_data = self.tab_info.get(index)
if tab_data and tab_data.get('ssh_manager'):
# 停止心跳但保持连接在池中
tab_data['ssh_manager'].stop_heartbeat()
# 移除标签页
self.tab_widget.removeTab(index)
# 移除存储的信息
if index in self.tab_info:
del self.tab_info[index]
# 更新剩余标签页的索引
new_tab_info = {}
for i in range(1, self.tab_widget.count()):
if i >= index:
new_tab_info[i] = self.tab_info.get(i+1, {})
else:
new_tab_info[i] = self.tab_info.get(i, {})
self.tab_info = new_tab_info
def closeEvent(self, event):
"""窗口关闭时清理所有连接"""
# 关闭所有SSH连接
for key, ssh_manager in self.ssh_connection_pool.items():
try:
ssh_manager.disconnect()
logger.info(f"已关闭SSH连接: {key}")
except:
logger.error(f"关闭SSH连接失败: {key}")
# 清空连接池
self.ssh_connection_pool.clear()
# 调用父类方法
super().closeEvent(event)
# 其他方法保持不变...
```
### 优化说明
#### 1. `detail_page.py` 优化
- **线程池管理**:使用 `QThreadPool` 管理上传任务,限制同时上传数量
- **MD5文件校验**:添加文件上传前后的MD5校验
- **任务信号解耦**:使用 `UploadSignals` 类统一管理信号
- **自动清理**:`setAutoDelete(True)` 确保任务完成后自动释放资源
- **日志增强**:增加详细的MD5校验日志
#### 2. `server_manager.py` 优化
- **SSH连接池**:实现简单的连接池管理,复用活跃连接
- **连接有效性检查**:获取连接时验证连接是否有效
- **连接生命周期管理**:
- 标签页关闭时只停止心跳,保持连接在池中
- 主窗口关闭时清理所有连接
- **异常处理优化**:统一处理连接异常
这些优化显著提升了系统的性能和可靠性,特别是在多服务器管理和大文件传输场景下。