Files
djangohelper/gunicorn_tab.py

646 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import os
from loguru import logger
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
QPushButton, QComboBox, QMessageBox, QTextEdit,
QGroupBox, QGridLayout, QProgressBar, QDialog,
QDialogButtonBox)
from PySide6.QtCore import Qt
from threads import (GunicornInstallThread, GunicornTestThread,
UploadGunicornServiceThread, ManageGunicornServiceThread)
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()
# 连接服务器切换信号
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):
"""加载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', '')
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}")
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
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}")
except Exception as e:
logger.error(f"加载Gunicorn配置失败: {str(e)}")
# 不显示警告,避免影响用户体验
def generate_service_file_from_config(self, config):
"""根据config.json配置生成服务文件内容"""
username = config.get('username', 'www-data')
project_name = config.get('project_name', 'myproject')
remote_directory = config.get('remote_directory', '/home/user')
django_path = config.get('django_path', remote_directory)
# 构建完整的项目路径
project_path = f"{django_path.rstrip('/')}"
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:8000 \\
--workers 4 \\
--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 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 \\
--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
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()
logger.info(f"从server_connection_tab获取密码长度: {len(password) if password else 0}")
# 如果密码为空,弹出密码输入对话框
if not password:
logger.info("密码为空,弹出密码输入对话框")
dialog = PasswordDialog(self)
if dialog.exec_() == QDialog.Accepted:
password = dialog.get_password()
logger.info(f"从对话框获取密码,长度: {len(password) if password else 0}")
# 保存密码到服务器连接标签页
if self.parent and hasattr(self.parent, 'server_connection_tab'):
self.parent.server_connection_tab.password_input.setText(password)
else:
logger.warning("用户取消了密码输入")
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)
# 获取密码
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):
"""测试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
# 获取端口配置
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}")
# 记录测试参数
logger.info(f"开始测试GunicornDjango路径: {django_path}, 端口: {port}, 主机: {host}")
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)
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()
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
# 获取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:
QMessageBox.warning(self, "警告", "请编辑服务文件内容")
return
self.output_text.append(f"正在上传服务文件 {service_name}...")
self.upload_service_btn.setEnabled(False)
# 获取密码
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
# 获取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}"
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)
# 获取密码
password = self.get_password()
if password is None:
logger.warning("未获取到密码,取消服务操作")
for btn in buttons:
btn.setEnabled(True)
return
# 获取端口配置
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()
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}")
# 如果是密码相关错误,提供更详细的提示
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
# 获取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)}")