Files
djangohelper/gunicorn_tab.py

646 lines
27 KiB
Python
Raw Permalink Normal View History

import os
from loguru import logger
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QComboBox, QMessageBox, QTextEdit,
2025-08-28 22:30:17 +08:00
QGroupBox, QGridLayout, QProgressBar, QDialog,
QDialogButtonBox)
from PySide6.QtCore import Qt
from threads import (GunicornInstallThread, GunicornTestThread,
UploadGunicornServiceThread, ManageGunicornServiceThread)
2025-08-28 22:30:17 +08:00
class PasswordDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("输入密码")
self.setMinimumWidth(300)
layout = QVBoxLayout()
# 密码输入
password_layout = QHBoxLayout()
password_layout.addWidget(QLabel("密码:"))
self.password_input = QLineEdit()
self.password_input.setEchoMode(QLineEdit.Password)
password_layout.addWidget(self.password_input)
layout.addLayout(password_layout)
# 按钮
button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout.addWidget(button_box)
self.setLayout(layout)
def get_password(self):
return self.password_input.text()
class GunicornTab(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.parent = parent
self.init_ui()
2025-08-28 22:30:17 +08:00
# 连接服务器切换信号
if self.parent and hasattr(self.parent, 'server_changed'):
self.parent.server_changed.connect(self.on_server_changed)
logger.info("Gunicorn标签已连接到服务器切换信号")
def init_ui(self):
layout = QVBoxLayout()
# Gunicorn配置组
config_group = QGroupBox("Gunicorn配置")
config_layout = QGridLayout()
config_layout.addWidget(QLabel("Django项目路径:"), 0, 0)
self.django_path_input = QLineEdit()
self.django_path_input.setPlaceholderText("/home/user/django_project")
config_layout.addWidget(self.django_path_input, 0, 1)
config_layout.addWidget(QLabel("服务名称:"), 1, 0)
self.service_name_input = QLineEdit("django_gunicorn")
config_layout.addWidget(self.service_name_input, 1, 1)
config_layout.addWidget(QLabel("端口:"), 2, 0)
self.port_input = QLineEdit("8000")
config_layout.addWidget(self.port_input, 2, 1)
config_layout.addWidget(QLabel("工作进程数:"), 3, 0)
self.workers_input = QLineEdit("3")
config_layout.addWidget(self.workers_input, 3, 1)
self.load_config_btn = QPushButton("加载配置")
self.load_config_btn.clicked.connect(self.load_gunicorn_config)
config_layout.addWidget(self.load_config_btn, 4, 1)
config_group.setLayout(config_layout)
layout.addWidget(config_group)
# Gunicorn操作组
gunicorn_group = QGroupBox("Gunicorn操作")
gunicorn_layout = QGridLayout()
self.install_gunicorn_btn = QPushButton("安装Gunicorn")
self.install_gunicorn_btn.clicked.connect(self.install_gunicorn)
gunicorn_layout.addWidget(self.install_gunicorn_btn, 0, 0)
self.test_gunicorn_btn = QPushButton("测试Gunicorn")
self.test_gunicorn_btn.clicked.connect(self.test_gunicorn)
gunicorn_layout.addWidget(self.test_gunicorn_btn, 0, 1)
self.upload_service_btn = QPushButton("上传服务文件")
self.upload_service_btn.clicked.connect(self.upload_service_file)
gunicorn_layout.addWidget(self.upload_service_btn, 1, 0)
self.start_service_btn = QPushButton("启动服务")
self.start_service_btn.clicked.connect(lambda: self.manage_service("start"))
gunicorn_layout.addWidget(self.start_service_btn, 1, 1)
self.stop_service_btn = QPushButton("停止服务")
self.stop_service_btn.clicked.connect(lambda: self.manage_service("stop"))
gunicorn_layout.addWidget(self.stop_service_btn, 2, 0)
self.restart_service_btn = QPushButton("重启服务")
self.restart_service_btn.clicked.connect(lambda: self.manage_service("restart"))
gunicorn_layout.addWidget(self.restart_service_btn, 2, 1)
self.status_service_btn = QPushButton("查看服务状态")
self.status_service_btn.clicked.connect(lambda: self.manage_service("status"))
gunicorn_layout.addWidget(self.status_service_btn, 3, 0)
self.enable_service_btn = QPushButton("启用开机自启")
self.enable_service_btn.clicked.connect(lambda: self.manage_service("enable"))
gunicorn_layout.addWidget(self.enable_service_btn, 3, 1)
gunicorn_group.setLayout(gunicorn_layout)
layout.addWidget(gunicorn_group)
# 服务文件编辑器
service_group = QGroupBox("Gunicorn服务文件")
service_layout = QVBoxLayout()
self.service_editor = QTextEdit()
self.service_editor.setPlaceholderText("Gunicorn服务文件内容将在这里显示...")
service_layout.addWidget(self.service_editor)
service_group.setLayout(service_layout)
layout.addWidget(service_group)
# 操作输出
output_group = QGroupBox("操作输出")
output_layout = QVBoxLayout()
self.output_text = QTextEdit()
self.output_text.setReadOnly(True)
self.output_text.setPlaceholderText("操作结果将在这里显示...")
output_layout.addWidget(self.output_text)
# 进度条
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
output_layout.addWidget(self.progress_bar)
output_group.setLayout(output_layout)
layout.addWidget(output_group)
# 日志查看组
log_group = QGroupBox("日志查看")
log_layout = QGridLayout()
self.view_access_log_btn = QPushButton("查看访问日志")
self.view_access_log_btn.clicked.connect(self.view_access_log)
log_layout.addWidget(self.view_access_log_btn, 0, 0)
self.view_error_log_btn = QPushButton("查看错误日志")
self.view_error_log_btn.clicked.connect(self.view_error_log)
log_layout.addWidget(self.view_error_log_btn, 0, 1)
log_group.setLayout(log_layout)
layout.addWidget(log_group)
layout.addStretch()
self.setLayout(layout)
# 加载Gunicorn配置
self.load_gunicorn_config()
def load_gunicorn_config(self):
2025-08-28 22:30:17 +08:00
"""加载Gunicorn配置使用config.json中的值"""
try:
if self.parent and hasattr(self.parent, 'get_current_config'):
config = self.parent.get_current_config()
if config:
# 使用remote_directory而不是django_path
django_path = config.get('remote_directory', '')
2025-08-28 22:30:17 +08:00
project_name = config.get('project_name', 'myproject')
username = config.get('username', 'www-data')
# 设置Django项目路径
self.django_path_input.setText(django_path)
# 设置服务名称为项目名
self.service_name_input.setText(project_name)
# 生成服务文件内容
service_content = self.generate_service_file_from_config(config)
self.service_editor.setText(service_content)
logger.info(f"从当前服务器配置加载Gunicorn配置: remote_directory={django_path}, project_name={project_name}")
2025-08-28 22:30:17 +08:00
else:
logger.warning("未找到当前服务器配置")
else:
# 兼容旧的加载方式
config_path = os.path.join(os.path.dirname(__file__), 'config.json')
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
if config and 'servers' in config and len(config['servers']) > 0:
server_config = config['servers'][0]
# 使用remote_directory
2025-08-28 22:30:17 +08:00
django_path = server_config.get('remote_directory', '')
project_name = server_config.get('project_name', 'myproject')
username = server_config.get('username', 'www-data')
# 设置Django项目路径
self.django_path_input.setText(django_path)
# 设置服务名称为项目名
self.service_name_input.setText(project_name)
# 生成服务文件内容
service_content = self.generate_service_file_from_config(server_config)
self.service_editor.setText(service_content)
logger.info(f"从配置文件加载Gunicorn配置: remote_directory={django_path}, project_name={project_name}")
2025-08-28 22:30:17 +08:00
except Exception as e:
logger.error(f"加载Gunicorn配置失败: {str(e)}")
# 不显示警告,避免影响用户体验
2025-08-28 22:30:17 +08:00
def generate_service_file_from_config(self, config):
"""根据config.json配置生成服务文件内容"""
username = config.get('username', 'www-data')
project_name = config.get('project_name', 'myproject')
2025-08-29 21:33:35 +08:00
remote_directory = config.get('remote_directory', '/home/user')
django_path = config.get('django_path', remote_directory)
2025-08-28 22:30:17 +08:00
# 构建完整的项目路径
project_path = f"{django_path.rstrip('/')}"
2025-08-28 22:30:17 +08:00
return f"""[Unit]
2025-08-28 22:30:17 +08:00
Description=Gunicorn daemon for {project_name}
After=network.target
[Service]
2025-08-28 22:30:17 +08:00
User={username}
Group={username}
WorkingDirectory={project_path}
# 所有Gunicorn参数直接在这里配置
ExecStart=/usr/local/bin/gunicorn \\
2025-08-29 21:33:35 +08:00
--bind 0.0.0.0:8000 \\
--workers 4 \\
2025-08-28 22:30:17 +08:00
--worker-class sync \\
--name {project_name} \\
--access-logfile {project_path}/logs/gunicorn_access.log \\
--error-logfile {project_path}/logs/gunicorn_error.log \\
--log-level info \\
{project_name}.wsgi:application
Restart=on-failure
RestartSec=5s
PrivateTmp=true
[Install]
2025-08-28 22:30:17 +08:00
WantedBy=multi-user.target"""
def generate_service_file(self, service_name, django_path, port, workers):
"""保持向后兼容的方法"""
# 获取config.json中的配置信息
config = None
if self.parent and hasattr(self.parent, 'server_connection_tab'):
config = self.parent.server_connection_tab.get_current_config()
if config:
return self.generate_service_file_from_config(config)
else:
# 如果没有config使用默认值
username = 'www-data'
project_name = service_name
project_path = f"{django_path.rstrip('/')}/{project_name}"
return f"""[Unit]
Description=Gunicorn daemon for {project_name}
After=network.target
[Service]
User={username}
Group={username}
WorkingDirectory={project_path}
# 所有Gunicorn参数直接在这里配置
ExecStart=/usr/local/bin/gunicorn \\
--bind 0.0.0.0:{port} \\
--workers 4 \\
2025-08-28 22:30:17 +08:00
--worker-class sync \\
--name {project_name} \\
--access-logfile {project_path}/logs/gunicorn_access.log \\
--error-logfile {project_path}/logs/gunicorn_error.log \\
--log-level info \\
{project_name}.wsgi:application
Restart=on-failure
RestartSec=5s
PrivateTmp=true
[Install]
WantedBy=multi-user.target"""
def check_ssh_connection(self):
if not self.parent or not self.parent.ssh_client:
QMessageBox.warning(self, "警告", "请先连接服务器")
return False
return True
2025-08-28 22:30:17 +08:00
def get_password(self):
# 获取密码
password = None
if self.parent and hasattr(self.parent, 'server_connection_tab'):
password = self.parent.server_connection_tab.password_input.text()
2025-08-29 21:33:35 +08:00
logger.info(f"从server_connection_tab获取密码长度: {len(password) if password else 0}")
2025-08-28 22:30:17 +08:00
# 如果密码为空,弹出密码输入对话框
if not password:
2025-08-29 21:33:35 +08:00
logger.info("密码为空,弹出密码输入对话框")
2025-08-28 22:30:17 +08:00
dialog = PasswordDialog(self)
if dialog.exec_() == QDialog.Accepted:
password = dialog.get_password()
2025-08-29 21:33:35 +08:00
logger.info(f"从对话框获取密码,长度: {len(password) if password else 0}")
2025-08-28 22:30:17 +08:00
# 保存密码到服务器连接标签页
if self.parent and hasattr(self.parent, 'server_connection_tab'):
self.parent.server_connection_tab.password_input.setText(password)
else:
2025-08-29 21:33:35 +08:00
logger.warning("用户取消了密码输入")
2025-08-28 22:30:17 +08:00
return None
return password
def install_gunicorn(self):
if not self.check_ssh_connection():
return
self.output_text.append("正在安装Gunicorn...")
self.install_gunicorn_btn.setEnabled(False)
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
# 获取密码
2025-08-28 22:30:17 +08:00
password = self.get_password()
if password is None:
self.install_gunicorn_btn.setEnabled(True)
self.progress_bar.setVisible(False)
return
self.gunicorn_install_thread = GunicornInstallThread(self.parent.ssh_client, password)
self.gunicorn_install_thread.progress_updated.connect(self.update_progress)
self.gunicorn_install_thread.result_ready.connect(self.on_install_gunicorn_result)
self.gunicorn_install_thread.start()
def update_progress(self, value):
self.progress_bar.setValue(value)
def on_install_gunicorn_result(self, success, message):
self.install_gunicorn_btn.setEnabled(True)
self.progress_bar.setVisible(False)
if success:
self.output_text.append(f"Gunicorn安装成功: {message}")
logger.info(f"Gunicorn安装成功: {message}")
else:
self.output_text.append(f"Gunicorn安装失败: {message}")
logger.error(f"Gunicorn安装失败: {message}")
def test_gunicorn(self):
2025-08-29 21:33:35 +08:00
"""测试Gunicorn配置"""
if not self.check_ssh_connection():
return
django_path = self.django_path_input.text().strip()
if not django_path:
QMessageBox.warning(self, "警告", "请输入Django项目路径")
return
2025-08-29 21:33:35 +08:00
# 获取端口配置
port = self.port_input.text().strip() or "8000"
# 获取主机地址配置
host = "127.0.0.1" # 默认值
if self.parent and hasattr(self.parent, 'get_current_config'):
config = self.parent.get_current_config()
if config and 'host' in config:
host = config['host']
logger.info(f"从配置获取主机地址: {host}")
2025-08-29 21:33:35 +08:00
# 记录测试参数
logger.info(f"开始测试GunicornDjango路径: {django_path}, 端口: {port}, 主机: {host}")
2025-08-29 21:33:35 +08:00
self.output_text.append(f"正在测试Gunicorn {django_path} (端口: {port}, 主机: {host})...")
self.test_gunicorn_btn.setEnabled(False)
self.progress_bar.setVisible(True)
self.progress_bar.setValue(0)
2025-08-29 21:33:35 +08:00
logger.info("创建Gunicorn测试线程")
# 注意GunicornTestThread构造函数可能需要更新以接受端口参数
# 确保测试时使用的端口与实际服务启动时一致
self.gunicorn_test_thread = GunicornTestThread(self.parent.ssh_client, django_path, port, host)
self.gunicorn_test_thread.progress_updated.connect(self.update_progress)
self.gunicorn_test_thread.result_ready.connect(self.on_test_gunicorn_result)
self.gunicorn_test_thread.start()
2025-08-29 21:33:35 +08:00
logger.info("Gunicorn测试线程已启动")
def on_test_gunicorn_result(self, success, message):
self.test_gunicorn_btn.setEnabled(True)
self.progress_bar.setVisible(False)
if success:
self.output_text.append(f"Gunicorn测试成功: {message}")
logger.info(f"Gunicorn测试成功: {message}")
else:
self.output_text.append(f"Gunicorn测试失败: {message}")
logger.error(f"Gunicorn测试失败: {message}")
def upload_service_file(self):
if not self.check_ssh_connection():
return
2025-08-28 22:30:17 +08:00
# 获取config.json中的配置信息
config = None
if self.parent and hasattr(self.parent, 'server_connection_tab'):
config = self.parent.server_connection_tab.get_current_config()
if config:
project_name = config.get('project_name', 'django')
else:
project_name = 'django'
# 使用gunicorn_[project_name].service格式作为服务名称
service_name = f"gunicorn_{project_name}"
service_content = self.service_editor.toPlainText()
if not service_name or not service_content:
2025-08-28 22:30:17 +08:00
QMessageBox.warning(self, "警告", "请编辑服务文件内容")
return
self.output_text.append(f"正在上传服务文件 {service_name}...")
self.upload_service_btn.setEnabled(False)
# 获取密码
2025-08-28 22:30:17 +08:00
password = self.get_password()
if password is None:
self.upload_service_btn.setEnabled(True)
return
self.upload_thread = UploadGunicornServiceThread(self.parent.ssh_client, service_name, service_content, password)
self.upload_thread.result_ready.connect(self.on_upload_service_result)
self.upload_thread.start()
def on_upload_service_result(self, success, message):
self.upload_service_btn.setEnabled(True)
if success:
self.output_text.append(f"服务文件上传成功: {message}")
logger.info(f"服务文件上传成功: {message}")
else:
self.output_text.append(f"服务文件上传失败: {message}")
logger.error(f"服务文件上传失败: {message}")
def manage_service(self, action):
if not self.check_ssh_connection():
return
2025-08-28 22:30:17 +08:00
# 获取config.json中的配置信息
config = None
if self.parent and hasattr(self.parent, 'server_connection_tab'):
config = self.parent.server_connection_tab.get_current_config()
if config:
project_name = config.get('project_name', 'django')
else:
project_name = 'django'
# 使用gunicorn_[project_name].service格式作为服务名称
service_name = f"gunicorn_{project_name}"
2025-08-29 21:33:35 +08:00
logger.info(f"正在执行服务 {action} 操作,服务名称: {service_name}")
self.output_text.append(f"正在执行服务 {action} 操作...")
# 禁用所有服务管理按钮
buttons = [self.start_service_btn, self.stop_service_btn, self.restart_service_btn,
self.status_service_btn, self.enable_service_btn]
for btn in buttons:
btn.setEnabled(False)
# 获取密码
2025-08-28 22:30:17 +08:00
password = self.get_password()
if password is None:
2025-08-29 21:33:35 +08:00
logger.warning("未获取到密码,取消服务操作")
2025-08-28 22:30:17 +08:00
for btn in buttons:
btn.setEnabled(True)
return
2025-08-29 21:33:35 +08:00
# 获取端口配置
port = self.port_input.text().strip() or "8000"
logger.info(f"获取到密码,长度: {len(password)}创建ManageGunicornServiceThread线程")
logger.info(f"开始管理Gunicorn服务 - 服务名: {service_name}, 操作: {action}, 端口: {port}")
self.manage_thread = ManageGunicornServiceThread(self.parent.ssh_client, service_name, action, password, port)
self.manage_thread.result_ready.connect(lambda s, m: self.on_manage_service_result(s, m, buttons))
self.manage_thread.start()
2025-08-29 21:33:35 +08:00
logger.info(f"Gunicorn服务管理线程已启动 - 操作: {action}")
def on_manage_service_result(self, success, message, buttons):
# 重新启用所有服务管理按钮
for btn in buttons:
btn.setEnabled(True)
if success:
self.output_text.append(f"服务操作成功: {message}")
logger.info(f"服务操作成功: {message}")
else:
self.output_text.append(f"服务操作失败: {message}")
logger.error(f"服务操作失败: {message}")
2025-08-29 21:33:35 +08:00
# 如果是密码相关错误,提供更详细的提示
if "password" in message.lower() or "sudo" in message.lower():
self.output_text.append("提示请检查sudo密码是否正确或尝试重新输入密码")
logger.warning("检测到可能的密码问题,提示用户重新输入密码")
def on_server_changed(self):
self.load_gunicorn_config()
def check_service_status(self):
if not self.check_ssh_connection():
return
2025-08-28 22:30:17 +08:00
# 获取config.json中的配置信息
config = None
if self.parent and hasattr(self.parent, 'server_connection_tab'):
config = self.parent.server_connection_tab.get_current_config()
if config:
project_name = config.get('project_name', 'django')
else:
project_name = 'django'
# 使用gunicorn_[project_name].service格式作为服务名称
service_name = f"gunicorn_{project_name}"
self.manage_service("status")
def view_access_log(self):
"""查看Gunicorn访问日志"""
if not self.check_ssh_connection():
return
# 获取config.json中的配置信息
config = None
if self.parent and hasattr(self.parent, 'server_connection_tab'):
config = self.parent.server_connection_tab.get_current_config()
if config:
project_path = config.get('remote_directory', '/home/xiaji')
else:
project_path = '/home/xiaji'
# 使用动态路径
access_log_path = f"{project_path.rstrip('/')}/logs/gunicorn_access.log"
self.output_text.append(f"正在查看访问日志: {access_log_path}")
# 获取密码
password = self.get_password()
if password is None:
return
try:
# 首先检查日志文件是否存在,如果不存在则创建
log_dir = f"{project_path.rstrip('/')}/logs"
check_command = f"sudo test -f {access_log_path} || (sudo mkdir -p {log_dir} && sudo touch {access_log_path} && sudo chmod 644 {access_log_path})"
stdin, stdout, stderr = self.parent.ssh_client.exec_command(check_command)
stdin.write(password + '\n')
stdin.flush()
# 使用tail命令查看日志的最后50行
command = f"sudo tail -n 50 {access_log_path}"
stdin, stdout, stderr = self.parent.ssh_client.exec_command(command)
stdin.write(password + '\n')
stdin.flush()
output = stdout.read().decode('utf-8')
error = stderr.read().decode('utf-8')
if output:
self.output_text.append("=== Gunicorn访问日志 ===")
self.output_text.append(output)
logger.info(f"成功查看访问日志: {len(output)} 字符")
else:
self.output_text.append("访问日志为空")
if error and "sudo" not in error.lower():
self.output_text.append(f"错误信息: {error}")
logger.error(f"查看访问日志出错: {error}")
except Exception as e:
self.output_text.append(f"查看访问日志失败: {str(e)}")
logger.error(f"查看访问日志失败: {str(e)}")
def view_error_log(self):
"""查看Gunicorn错误日志"""
if not self.check_ssh_connection():
return
# 获取config.json中的配置信息
config = None
if self.parent and hasattr(self.parent, 'server_connection_tab'):
config = self.parent.server_connection_tab.get_current_config()
if config:
project_path = config.get('remote_directory', '/home/xiaji')
else:
project_path = '/home/xiaji'
# 使用动态路径
error_log_path = f"{project_path.rstrip('/')}/logs/gunicorn_error.log"
self.output_text.append(f"正在查看错误日志: {error_log_path}")
# 获取密码
password = self.get_password()
if password is None:
return
try:
# 首先检查日志文件是否存在,如果不存在则创建
log_dir = f"{project_path.rstrip('/')}/logs"
check_command = f"sudo test -f {error_log_path} || (sudo mkdir -p {log_dir} && sudo touch {error_log_path} && sudo chmod 644 {error_log_path})"
stdin, stdout, stderr = self.parent.ssh_client.exec_command(check_command)
stdin.write(password + '\n')
stdin.flush()
# 使用tail命令查看日志的最后50行
command = f"sudo tail -n 50 {error_log_path}"
stdin, stdout, stderr = self.parent.ssh_client.exec_command(command)
stdin.write(password + '\n')
stdin.flush()
output = stdout.read().decode('utf-8')
error = stderr.read().decode('utf-8')
if output:
self.output_text.append("=== Gunicorn错误日志 ===")
self.output_text.append(output)
logger.info(f"成功查看错误日志: {len(output)} 字符")
else:
self.output_text.append("错误日志为空")
if error and "sudo" not in error.lower():
self.output_text.append(f"错误信息: {error}")
logger.error(f"查看错误日志出错: {error}")
except Exception as e:
self.output_text.append(f"查看错误日志失败: {str(e)}")
logger.error(f"查看错误日志失败: {str(e)}")