412 lines
15 KiB
Python
412 lines
15 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
身体平衡评估系统 - 主启动脚本
|
||
|
||
这个脚本负责启动整个应用程序,包括后端服务和前端界面。
|
||
支持开发模式和生产模式。
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import time
|
||
import signal
|
||
import subprocess
|
||
import threading
|
||
import webbrowser
|
||
import logging
|
||
from pathlib import Path
|
||
|
||
# 添加项目根目录到Python路径
|
||
project_root = Path(__file__).parent.parent
|
||
sys.path.insert(0, str(project_root))
|
||
sys.path.insert(0, str(project_root / 'backend'))
|
||
|
||
from utils import Config, Logger
|
||
|
||
class ApplicationLauncher:
|
||
"""应用程序启动器"""
|
||
|
||
def __init__(self):
|
||
self.config = Config()
|
||
# 设置日志
|
||
Logger.setup_logging('INFO', 'logs/app.log')
|
||
self.logger = logging.getLogger('main')
|
||
self.backend_process = None
|
||
self.frontend_process = None
|
||
self.running = False
|
||
|
||
# 设置信号处理
|
||
signal.signal(signal.SIGINT, self._signal_handler)
|
||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||
|
||
def _signal_handler(self, signum, frame):
|
||
"""信号处理器"""
|
||
self.logger.info(f"接收到信号 {signum},正在关闭应用程序...")
|
||
self.stop()
|
||
|
||
def check_dependencies(self):
|
||
"""检查依赖项"""
|
||
self.logger.info("检查系统依赖项...")
|
||
|
||
# 检查Python版本
|
||
if sys.version_info < (3, 8):
|
||
self.logger.error("需要Python 3.8或更高版本")
|
||
return False
|
||
|
||
# 检查必要的Python包
|
||
required_packages = [
|
||
'flask', 'flask_cors', 'flask_socketio',
|
||
'numpy', 'pandas', 'opencv-python',
|
||
'sqlite3'
|
||
]
|
||
|
||
missing_packages = []
|
||
for package in required_packages:
|
||
try:
|
||
if package == 'sqlite3':
|
||
import sqlite3
|
||
elif package == 'opencv-python':
|
||
import cv2
|
||
elif package == 'flask_cors':
|
||
import flask_cors
|
||
elif package == 'flask_socketio':
|
||
import flask_socketio
|
||
else:
|
||
__import__(package)
|
||
except ImportError:
|
||
missing_packages.append(package)
|
||
|
||
if missing_packages:
|
||
self.logger.error(f"缺少必要的Python包: {', '.join(missing_packages)}")
|
||
self.logger.info("请运行: pip install -r backend/requirements.txt")
|
||
return False
|
||
|
||
# 检查Node.js和npm(用于前端开发)
|
||
if self.config.get('APP', 'mode', 'development') == 'development':
|
||
try:
|
||
result = subprocess.run(['node', '--version'],
|
||
capture_output=True, text=True)
|
||
if result.returncode != 0:
|
||
self.logger.warning("未找到Node.js,将跳过前端开发服务器")
|
||
else:
|
||
self.logger.info(f"Node.js版本: {result.stdout.strip()}")
|
||
except FileNotFoundError:
|
||
self.logger.warning("未找到Node.js,将跳过前端开发服务器")
|
||
|
||
self.logger.info("依赖项检查完成")
|
||
return True
|
||
|
||
def setup_directories(self):
|
||
"""设置必要的目录"""
|
||
self.logger.info("设置应用程序目录...")
|
||
|
||
directories = [
|
||
'data',
|
||
'data/patients',
|
||
'data/sessions',
|
||
'data/exports',
|
||
'data/backups',
|
||
'logs',
|
||
'temp'
|
||
]
|
||
|
||
for directory in directories:
|
||
dir_path = project_root / directory
|
||
dir_path.mkdir(parents=True, exist_ok=True)
|
||
self.logger.debug(f"创建目录: {dir_path}")
|
||
|
||
self.logger.info("目录设置完成")
|
||
|
||
def start_backend(self):
|
||
"""启动后端服务"""
|
||
self.logger.info("启动后端服务...")
|
||
|
||
backend_script = project_root / 'backend' / 'app.py'
|
||
if not backend_script.exists():
|
||
self.logger.error(f"后端脚本不存在: {backend_script}")
|
||
return False
|
||
|
||
try:
|
||
# 设置环境变量
|
||
env = os.environ.copy()
|
||
env['PYTHONPATH'] = str(project_root)
|
||
env['FLASK_APP'] = str(backend_script)
|
||
|
||
if self.config.get('APP', 'mode', 'development') == 'development':
|
||
env['FLASK_ENV'] = 'development'
|
||
env['FLASK_DEBUG'] = '1'
|
||
else:
|
||
env['FLASK_ENV'] = 'production'
|
||
env['FLASK_DEBUG'] = '0'
|
||
|
||
# 启动后端进程
|
||
cmd = [
|
||
sys.executable,
|
||
str(backend_script),
|
||
'--host', self.config.get('SERVER', 'host', '127.0.0.1'),
|
||
'--port', self.config.get('SERVER', 'port', '5000')
|
||
]
|
||
|
||
self.backend_process = subprocess.Popen(
|
||
cmd,
|
||
env=env,
|
||
cwd=str(project_root),
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE,
|
||
text=True
|
||
)
|
||
|
||
self.logger.info(f"后端服务已启动 (PID: {self.backend_process.pid})")
|
||
|
||
# 等待后端服务启动
|
||
self._wait_for_backend()
|
||
return True
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"启动后端服务失败: {e}")
|
||
return False
|
||
|
||
def _wait_for_backend(self, timeout=30):
|
||
"""等待后端服务启动"""
|
||
import requests
|
||
|
||
backend_url = f"http://{self.config.get('SERVER', 'host', '127.0.0.1')}:{self.config.get('SERVER', 'port', '5000')}"
|
||
health_url = f"{backend_url}/api/health"
|
||
|
||
self.logger.info("等待后端服务启动...")
|
||
|
||
start_time = time.time()
|
||
while time.time() - start_time < timeout:
|
||
try:
|
||
response = requests.get(health_url, timeout=2)
|
||
if response.status_code == 200:
|
||
self.logger.info("后端服务已就绪")
|
||
return True
|
||
except requests.exceptions.RequestException:
|
||
pass
|
||
|
||
time.sleep(1)
|
||
|
||
self.logger.warning("后端服务启动超时")
|
||
return False
|
||
|
||
def start_frontend_dev(self):
|
||
"""启动前端开发服务器"""
|
||
if self.config.get('APP', 'mode', 'development') != 'development':
|
||
return True
|
||
|
||
self.logger.info("启动前端开发服务器...")
|
||
|
||
frontend_dir = project_root / 'frontend' / 'src' / 'renderer'
|
||
if not frontend_dir.exists():
|
||
self.logger.warning("前端目录不存在,跳过前端开发服务器")
|
||
return True
|
||
|
||
package_json = frontend_dir / 'package.json'
|
||
if not package_json.exists():
|
||
self.logger.warning("package.json不存在,跳过前端开发服务器")
|
||
return True
|
||
|
||
try:
|
||
# 检查是否已安装依赖
|
||
node_modules = frontend_dir / 'node_modules'
|
||
if not node_modules.exists():
|
||
self.logger.info("安装前端依赖...")
|
||
install_process = subprocess.run(
|
||
['npm', 'install'],
|
||
cwd=str(frontend_dir),
|
||
capture_output=True,
|
||
text=True,
|
||
shell=True
|
||
)
|
||
|
||
if install_process.returncode != 0:
|
||
self.logger.error(f"安装前端依赖失败: {install_process.stderr}")
|
||
return False
|
||
|
||
# 启动开发服务器
|
||
self.frontend_process = subprocess.Popen(
|
||
['npm', 'run', 'dev'],
|
||
cwd=str(frontend_dir),
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.PIPE,
|
||
text=True,
|
||
shell=True
|
||
)
|
||
|
||
self.logger.info(f"前端开发服务器已启动 (PID: {self.frontend_process.pid})")
|
||
return True
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"启动前端开发服务器失败: {e}")
|
||
return False
|
||
|
||
def open_browser(self):
|
||
"""打开浏览器"""
|
||
if not self.config.getboolean('APP', 'auto_open_browser', True):
|
||
return
|
||
|
||
if self.config.get('APP', 'mode', 'development') == 'development':
|
||
# 开发模式下打开前端开发服务器
|
||
url = "http://localhost:3000" # Vite配置端口
|
||
else:
|
||
# 生产模式下打开后端服务
|
||
url = f"http://{self.config.get('SERVER', 'host', '127.0.0.1')}:{self.config.get('SERVER', 'port', '5000')}"
|
||
|
||
def delayed_open():
|
||
time.sleep(3) # 等待服务启动
|
||
try:
|
||
webbrowser.open(url)
|
||
self.logger.info(f"已打开浏览器: {url}")
|
||
except Exception as e:
|
||
self.logger.warning(f"打开浏览器失败: {e}")
|
||
|
||
threading.Thread(target=delayed_open, daemon=True).start()
|
||
|
||
def monitor_processes(self):
|
||
"""监控子进程"""
|
||
while self.running:
|
||
try:
|
||
# 检查后端进程
|
||
if self.backend_process and self.backend_process.poll() is not None:
|
||
self.logger.error("后端进程意外退出")
|
||
if self.backend_process.returncode != 0:
|
||
stderr = self.backend_process.stderr.read()
|
||
if stderr:
|
||
self.logger.error(f"后端错误: {stderr}")
|
||
self.running = False
|
||
break
|
||
|
||
# 检查前端进程
|
||
if (self.frontend_process and
|
||
self.frontend_process.poll() is not None and
|
||
self.config.get('APP', 'mode', 'development') == 'development'):
|
||
self.logger.warning("前端开发服务器意外退出")
|
||
|
||
time.sleep(5)
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"进程监控错误: {e}")
|
||
break
|
||
|
||
def start(self):
|
||
"""启动应用程序"""
|
||
self.logger.info("=" * 50)
|
||
self.logger.info("身体平衡评估系统启动中...")
|
||
self.logger.info(f"模式: {self.config.get('APP', 'mode', 'development')}")
|
||
self.logger.info("=" * 50)
|
||
|
||
# 检查依赖项
|
||
if not self.check_dependencies():
|
||
return False
|
||
|
||
# 设置目录
|
||
self.setup_directories()
|
||
|
||
# 启动后端服务
|
||
if not self.start_backend():
|
||
return False
|
||
|
||
# 启动前端开发服务器(仅开发模式)
|
||
if not self.start_frontend_dev():
|
||
self.logger.warning("前端开发服务器启动失败,但继续运行")
|
||
|
||
self.running = True
|
||
|
||
# 打开浏览器
|
||
self.open_browser()
|
||
|
||
# 启动进程监控
|
||
monitor_thread = threading.Thread(target=self.monitor_processes, daemon=True)
|
||
monitor_thread.start()
|
||
|
||
self.logger.info("应用程序启动完成")
|
||
self.logger.info(f"后端服务: http://{self.config.get('SERVER', 'host', '127.0.0.1')}:{self.config.get('SERVER', 'port', '5000')}")
|
||
|
||
if self.config.get('APP', 'mode', 'development') == 'development':
|
||
self.logger.info("前端开发服务器: http://localhost:3000")
|
||
|
||
self.logger.info("按 Ctrl+C 退出应用程序")
|
||
|
||
# 主循环
|
||
try:
|
||
while self.running:
|
||
time.sleep(1)
|
||
except KeyboardInterrupt:
|
||
self.logger.info("接收到中断信号")
|
||
|
||
self.stop()
|
||
return True
|
||
|
||
def stop(self):
|
||
"""停止应用程序"""
|
||
if not self.running:
|
||
return
|
||
|
||
self.logger.info("正在停止应用程序...")
|
||
self.running = False
|
||
|
||
# 停止前端进程
|
||
if self.frontend_process:
|
||
try:
|
||
self.frontend_process.terminate()
|
||
self.frontend_process.wait(timeout=5)
|
||
self.logger.info("前端开发服务器已停止")
|
||
except subprocess.TimeoutExpired:
|
||
self.frontend_process.kill()
|
||
self.logger.warning("强制终止前端开发服务器")
|
||
except Exception as e:
|
||
self.logger.error(f"停止前端服务器失败: {e}")
|
||
|
||
# 停止后端进程
|
||
if self.backend_process:
|
||
try:
|
||
self.backend_process.terminate()
|
||
self.backend_process.wait(timeout=10)
|
||
self.logger.info("后端服务已停止")
|
||
except subprocess.TimeoutExpired:
|
||
self.backend_process.kill()
|
||
self.logger.warning("强制终止后端服务")
|
||
except Exception as e:
|
||
self.logger.error(f"停止后端服务失败: {e}")
|
||
|
||
self.logger.info("应用程序已停止")
|
||
|
||
def main():
|
||
"""主函数"""
|
||
import argparse
|
||
|
||
parser = argparse.ArgumentParser(description='身体平衡评估系统')
|
||
parser.add_argument('--mode', choices=['development', 'production'],
|
||
default='development', help='运行模式')
|
||
parser.add_argument('--host', default='127.0.0.1', help='服务器主机')
|
||
parser.add_argument('--port', type=int, default=5000, help='服务器端口')
|
||
parser.add_argument('--no-browser', action='store_true', help='不自动打开浏览器')
|
||
parser.add_argument('--log-level', choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
|
||
default='INFO', help='日志级别')
|
||
|
||
args = parser.parse_args()
|
||
|
||
# 更新配置
|
||
config = Config()
|
||
config.set('APP', 'mode', args.mode)
|
||
config.set('APP', 'auto_open_browser', str(not args.no_browser))
|
||
config.set('SERVER', 'host', args.host)
|
||
config.set('SERVER', 'port', str(args.port))
|
||
|
||
# 设置日志级别
|
||
import logging
|
||
logging.getLogger().setLevel(getattr(logging, args.log_level))
|
||
|
||
# 启动应用程序
|
||
launcher = ApplicationLauncher()
|
||
try:
|
||
success = launcher.start()
|
||
sys.exit(0 if success else 1)
|
||
except Exception as e:
|
||
print(f"启动失败: {e}")
|
||
sys.exit(1)
|
||
|
||
if __name__ == '__main__':
|
||
main() |